4
4
"""
5
5
CircuitPython Canary Day and Night Light with Optional Network-Down Detection
6
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.
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.
9
9
10
10
In the event that the internet connection fails, it will begin blinking red to notify you.
11
11
If the initial test ping fails, and the subsequent pings fail over 30 times, the board
12
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.
13
+ feature is enabled by default. It can easily be disabled at the beginning of the code.
15
14
"""
16
15
import os
17
16
import ssl
26
25
import neopixel
27
26
from adafruit_io .adafruit_io import IO_HTTP
28
27
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.
32
33
NETWORK_DOWN_DETECTION = True
33
34
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
39
41
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.
43
78
# Must be an integer between 0 and 23. Defaults to 20 (8pm).
44
- RED_TIME = 20
79
+ SLEEP_TIME = 20
45
80
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 .
49
84
# Must be an integer between 0 and 23. Defaults to 6 (6am).
50
- BLUE_TIME = 6
85
+ WAKE_TIME = 6
51
86
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
58
94
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!" )
63
99
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"
67
105
106
+ # ============ HARDWARE AND CODE SET UP ============
68
107
# Instantiate the NeoPixel object.
69
108
pixels = neopixel .NeoPixel (board .A3 , 25 )
70
109
71
110
111
+ # Create helper functions
72
112
def reload_on_error (delay , error_content = None , reload_type = "reload" ):
73
113
"""
74
114
Reset the board when an error is encountered.
@@ -102,17 +142,17 @@ def color_time(current_hour):
102
142
:param current_hour: Provide a time, hour only. The `tm_hour` part of the
103
143
`io.receive_time()` object is acceptable here.
104
144
"""
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
116
156
117
157
118
158
def blink (color ):
@@ -121,10 +161,10 @@ def blink(color):
121
161
122
162
:param tuple color: The color the LEDs will blink.
123
163
"""
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
126
166
else :
127
- pixels .brightness = BLUE_BRIGHTNESS
167
+ pixels .brightness = WAKE_BRIGHTNESS
128
168
pixels .fill (color )
129
169
time .sleep (0.5 )
130
170
pixels .fill ((0 , 0 , 0 ))
@@ -138,20 +178,23 @@ def blink(color):
138
178
pool = socketpool .SocketPool (wifi .radio )
139
179
requests = adafruit_requests .Session (pool , ssl .create_default_context ())
140
180
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,
142
182
# check your SSID and password before continuing.
143
183
print ("Wifi connection failed." )
144
184
reload_on_error (5 , error )
145
185
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.
149
189
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
155
198
156
199
# Set up Adafruit IO. This will provide the current time through `io.receive_time()`.
157
200
io = IO_HTTP (os .getenv ("aio_username" ), os .getenv ("aio_key" ), requests )
@@ -162,45 +205,44 @@ def blink(color):
162
205
try :
163
206
sundial = io .receive_time () # Create the sundial variable to keep the time.
164
207
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 )
174
212
175
- # Initialise various time tracking variables
213
+ # Initialise various time tracking variables.
176
214
ping_time = 0
177
215
check_time = 0
178
216
ping_fail_time = time .monotonic ()
179
217
180
- # Initialise ping fail count tracking
218
+ # Initialise ping fail count tracking.
181
219
ping_fail_count = 0
220
+
221
+ # ============ LOOP ============
182
222
while True :
183
223
# Resets current_time to the current time.monotonic() value every time through the loop.
184
224
current_time = time .monotonic ()
185
225
# WiFi and IO connections can fail arbitrarily. The bulk of the loop is included in a
186
226
# try/except block to ensure the project will continue to run unattended if any
187
227
# failures do occur.
188
228
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 :
191
231
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.
195
236
print (f"Pinging { ip_address } : { wifi_ping } ms" )
196
237
197
- # If the ping is successful.. .
238
+ # If the ping is successful, continue with this code .
198
239
if wifi_ping is not None :
199
240
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 :
202
243
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 ()
204
246
# Print the current date and time to the serial console.
205
247
print (f"LED color time-check. Date and time: { sundial .tm_year } -{ sundial .tm_mon } -" +
206
248
f"{ sundial .tm_mday } { sundial .tm_hour } :{ sundial .tm_min :02} " )
@@ -209,32 +251,29 @@ def blink(color):
209
251
# to `fill()` to set the LED color.
210
252
pixels .fill (color_time (sundial .tm_hour ))
211
253
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 .
216
258
print (f"Ping failed { ping_fail_count } times" )
217
259
# If network down detection is enabled, run the following code.
218
260
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 )
222
265
# If the initial setup ping failed, it means the network connection was failing
223
266
# 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.
225
269
if not initial_ping and ping_fail_count > 30 :
226
- reload_on_error (0 ) # ...immediately soft reload the board.
270
+ reload_on_error (0 )
227
271
# If the initial ping succeeded, the blinking will continue until the connection
228
272
# is reestablished and the pings are once again successful.
229
273
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 ============
235
275
# 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 .
238
278
except Exception as error : # pylint: disable=broad-except
239
- # ...wait 10 seconds and hard reset the board.
240
279
reload_on_error (10 , error , reload_type = "reset" )
0 commit comments