44"""
55CircuitPython 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
1010In the event that the internet connection fails, it will begin blinking red to notify you.
1111If the initial test ping fails, and the subsequent pings fail over 30 times, the board
1212will 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"""
1615import os
1716import ssl
2625import neopixel
2726from 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.
3233NETWORK_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.
69108pixels = neopixel .NeoPixel (board .A3 , 25 )
70109
71110
111+ # Create helper functions
72112def 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
118158def 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 ())
140180except 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.
149189wifi_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()`.
157200io = IO_HTTP (os .getenv ("aio_username" ), os .getenv ("aio_key" ), requests )
@@ -162,45 +205,44 @@ def blink(color):
162205try :
163206 sundial = io .receive_time () # Create the sundial variable to keep the time.
164207except 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.
176214ping_time = 0
177215check_time = 0
178216ping_fail_time = time .monotonic ()
179217
180- # Initialise ping fail count tracking
218+ # Initialise ping fail count tracking.
181219ping_fail_count = 0
220+
221+ # ============ LOOP ============
182222while 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