Skip to content

Commit ceb3a02

Browse files
committed
Canary nightlight code.
1 parent fd4e344 commit ceb3a02

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

Canary_Nightlight/code.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# SPDX-FileCopyrightText: 2023 Kattni Rembor for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
4+
"""
5+
CircuitPython Canary Day and Night Light with Optional Network-Down Detection
6+
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.
9+
10+
In the event that the internet connection fails, it will begin blinking red to notify you.
11+
If the initial test ping fails, and the subsequent pings fail over 30 times, the board
12+
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.
15+
"""
16+
import os
17+
import ssl
18+
import time
19+
import ipaddress
20+
import supervisor
21+
import board
22+
import wifi
23+
import microcontroller
24+
import socketpool
25+
import adafruit_requests
26+
import neopixel
27+
from adafruit_io.adafruit_io import IO_HTTP
28+
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.
32+
NETWORK_DOWN_DETECTION = True
33+
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
39+
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.
43+
# Must be an integer between 0 and 23. Defaults to 20 (8pm).
44+
RED_TIME = 20
45+
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.
49+
# Must be an integer between 0 and 23. Defaults to 6 (6am).
50+
BLUE_TIME = 6
51+
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
58+
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
63+
64+
# Define the light colors. The default colors are blue and red.
65+
BLUE = (0, 0, 255)
66+
RED = (255, 0, 0)
67+
68+
# Instantiate the NeoPixel object.
69+
pixels = neopixel.NeoPixel(board.A3, 25)
70+
71+
72+
def reload_on_error(delay, error_content=None, reload_type="reload"):
73+
"""
74+
Reset the board when an error is encountered.
75+
76+
:param float delay: The delay in seconds before the board should reset.
77+
:param Exception error_content: The error encountered. Used to print the error before reset.
78+
:param str reload_type: The type of reload desired. Defaults to "reload", which invokes
79+
``supervisor.reload()`` to soft-reload the board. To hard reset
80+
the board, set this to "reset", which invokes
81+
``microcontroller.reset()``.
82+
"""
83+
if str(reload_type).lower().strip() not in ["reload", "reset"]:
84+
raise ValueError("Invalid reload type:", reload_type)
85+
if error_content:
86+
print("Error:\n", str(error_content))
87+
if delay:
88+
print(
89+
f"{reload_type[0].upper() + reload_type[1:]} microcontroller in {delay} seconds."
90+
)
91+
time.sleep(delay)
92+
if reload_type == "reload":
93+
supervisor.reload()
94+
if reload_type == "reset":
95+
microcontroller.reset()
96+
97+
98+
def color_time(current_hour):
99+
"""
100+
Verifies what color the LEDs should be based on the time.
101+
102+
:param current_hour: Provide a time, hour only. The `tm_hour` part of the
103+
`io.receive_time()` object is acceptable here.
104+
"""
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
116+
117+
118+
def blink(color):
119+
"""
120+
Blink the NeoPixel LEDs a specific color.
121+
122+
:param tuple color: The color the LEDs will blink.
123+
"""
124+
if color_time(sundial.tm_hour) == RED:
125+
pixels.brightness = RED_BRIGHTNESS
126+
else:
127+
pixels.brightness = BLUE_BRIGHTNESS
128+
pixels.fill(color)
129+
time.sleep(0.5)
130+
pixels.fill((0, 0, 0))
131+
time.sleep(0.5)
132+
133+
134+
# Connect to WiFi. This process can fail for various reasons. It is included in a try/except
135+
# block to ensure the project continues running when unattended.
136+
try:
137+
wifi.radio.connect(os.getenv("wifi_ssid"), os.getenv("wifi_password"))
138+
pool = socketpool.SocketPool(wifi.radio)
139+
requests = adafruit_requests.Session(pool, ssl.create_default_context())
140+
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,
142+
# check your SSID and password before continuing.
143+
print("Wifi connection failed.")
144+
reload_on_error(5, error)
145+
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")
149+
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.
155+
156+
# Set up Adafruit IO. This will provide the current time through `io.receive_time()`.
157+
io = IO_HTTP(os.getenv("aio_username"), os.getenv("aio_key"), requests)
158+
159+
# Retrieve the time on startup. This is included to verify that the Adafruit IO set
160+
# up was successful. This process can fail for various reasons. It is included in a
161+
# try/except block to ensure the project continues to run when unattended.
162+
try:
163+
sundial = io.receive_time() # Create the sundial variable to keep the time.
164+
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
174+
175+
# Initialise various time tracking variables
176+
ping_time = 0
177+
check_time = 0
178+
ping_fail_time = time.monotonic()
179+
180+
# Initialise ping fail count tracking
181+
ping_fail_count = 0
182+
while True:
183+
# Resets current_time to the current time.monotonic() value every time through the loop.
184+
current_time = time.monotonic()
185+
# WiFi and IO connections can fail arbitrarily. The bulk of the loop is included in a
186+
# try/except block to ensure the project will continue to run unattended if any
187+
# failures do occur.
188+
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:
191+
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.
195+
print(f"Pinging {ip_address}: {wifi_ping} ms")
196+
197+
# If the ping is successful...
198+
if wifi_ping is not None:
199+
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:
202+
check_time = time.monotonic()
203+
sundial = io.receive_time() # Retrieve the time and save it to sundial.
204+
# Print the current date and time to the serial console.
205+
print(f"LED color time-check. Date and time: {sundial.tm_year}-{sundial.tm_mon}-" +
206+
f"{sundial.tm_mday} {sundial.tm_hour}:{sundial.tm_min:02}")
207+
# Provides the current hour to the color_time function. This verifies the
208+
# current color based on time and returns that color, which is provided
209+
# to `fill()` to set the LED color.
210+
pixels.fill(color_time(sundial.tm_hour))
211+
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.
216+
print(f"Ping failed {ping_fail_count} times")
217+
# If network down detection is enabled, run the following code.
218+
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.
222+
# If the initial setup ping failed, it means the network connection was failing
223+
# 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...
225+
if not initial_ping and ping_fail_count > 30:
226+
reload_on_error(0) # ...immediately soft reload the board.
227+
# If the initial ping succeeded, the blinking will continue until the connection
228+
# is reestablished and the pings are once again successful.
229+
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.
235+
# 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...
238+
except Exception as error: # pylint: disable=broad-except
239+
# ...wait 10 seconds and hard reset the board.
240+
reload_on_error(10, error, reload_type="reset")

0 commit comments

Comments
 (0)