You've customized all of the options you're interested in changing. Now, the rest of the code follows. Wondering what's going on with it? This page has you covered!
The first part of the code is only run once after the board starts up. It is used for getting everything set up.
import os import ssl import time import ipaddress import supervisor import board import wifi import microcontroller import socketpool import adafruit_requests import neopixel from adafruit_io.adafruit_io import IO_HTTP
Customization
The customization details can be found in the Customize Your Canary section of the Code the Canary page.
Value Checks
There are two time values that should not be set below a certain value, and must be an integer (whole number). Therefore, there are two checks in the customization code to ensure they are set properly.
There are two things each check is looking for.
- Is the provided value is less than the required minimum?
- Is the provided value is a float?
If either or both of these conditions are met, the code will stop running and raise an exception. For the code to continue running, you must update the value to an integer within the appropriate range.
if UP_PING_INTERVAL < 1 or isinstance(UP_PING_INTERVAL, float): raise ValueError("UP_PING_INTERVAL must be a integer, and greater than 1!")
Hardware and Code Set Up
There is very little hardware set up, but a pretty solid chunk of code setup is necessary.
NeoPixel Set Up
To tell the code where to look for the NeoPixel BFF and be able to control the NeoPixels, you need to instantiate the NeoPixel object. The BFF connects on pin A3
, and there are 25
LEDs in the grid.
pixels = neopixel.NeoPixel(board.A3, 25)
Helper Functions
There are three helper functions used in this code. In many situations, functions are included in demo code to create a simple way to use the same block of code multiple times later in the demo. They can also be used to make later code simpler by separating out a complicated code block, even if it's only going to be used once. The helper functions in this code fall into both categories.
Reload on Error
This function is used to reset the board when an error occurs. Some errors you want to stop the code so you can fix the issue. Other errors occur because of an issue beyond the code or board, and can sometimes be resolved by resetting the board. Both types are included in this demo. This function is specifically used for the ones where we want the board to reset to the code can continue running. This ensures that if an error occurs, such as the WiFi disconnects or the Adafruit IO connection fails, that you canary will simply reset and continue being a glowing friend.
The reload_on_error
function has three possible arguments: delay
, error_content
, and reload_type
.
-
delay
- This is the delay in seconds between when the error occurs and when the board resets. This is the only argument that is required for this function to work. -
error_content
- If an error string is provided, this is used to print the error to the serial console. If nothing is provided, nothing is printed. In this demo, typically the exception will beexcept Exception as error:
, and you would includeerror
after a comma following the delay. -
reload_type
- This determines which way this function will trigger a reload. There are two options: hardreset
, and softreload
. A hard reset is the same thing that happens when you press the physical reset button on the board. A soft reload is the same thing that happens when your code auto-reloads, or you press CTRL+C and CTRL+D in the serial console. Some issues are resolved with a soft reload, and others require a hard reset. Therefore, this function is designed to allow for both. It defaults to"reload"
(the quotes are necessary), meaning, with no changes, it will trigger a soft reload. To trigger a hard reset, update this argument to"reset"
.
def reload_on_error(delay, error_content=None, reload_type="reload"): if str(reload_type).lower().strip() not in ["reload", "reset"]: raise ValueError("Invalid reload type:", reload_type) if error_content: print("Error:\n", str(error_content)) if delay: print( f"{reload_type[0].upper() + reload_type[1:]} microcontroller in {delay} seconds." ) time.sleep(delay) if reload_type == "reload": supervisor.reload() if reload_type == "reset": microcontroller.reset()
Color Time
This function determines what color to illuminate your canary based on the current time. It is made up of many of the customisable variables from earlier in the code. This code is necessary because the wake time and sleep time might not always be in numerical order; sleep time could occur after wake time. This complicated the color check code significantly, and therefore made the most sense in a function.
The color_time
function has one argument.
-
current_hour
- This is the current time as an hour value only. In this code, the value here is alwayssundial.tm_hour
which is the hour value from the time returned when checking in with Adafruit IO's time service.
def color_time(current_hour): if WAKE_TIME < SLEEP_TIME: if WAKE_TIME <= current_hour < SLEEP_TIME: pixels.brightness = WAKE_BRIGHTNESS return WAKE_COLOR pixels.brightness = SLEEP_BRIGHTNESS return SLEEP_COLOR if SLEEP_TIME <= current_hour < WAKE_TIME: pixels.brightness = SLEEP_BRIGHTNESS return SLEEP_COLOR pixels.brightness = WAKE_BRIGHTNESS return WAKE_COLOR
Blink
This is used to blink the canary a specified color. It uses the color_time()
function to sort out whether to blink at the wake brightness of the sleep brightness; that way if the network goes down at night, it doesn't start blinking at full brightness.
The blink
function has one argument.
-
color
- This is the color to blink the LEDs. The color used in the code is customised at the top of the program.
def blink(color): if color_time(sundial.tm_hour) == SLEEP_COLOR: pixels.brightness = SLEEP_BRIGHTNESS else: pixels.brightness = WAKE_BRIGHTNESS pixels.fill(color) time.sleep(0.5) pixels.fill((0, 0, 0)) time.sleep(0.5)
Connect to WiFi
This section connects to the WiFi using your credentials from the settings.toml file. Then it creates the socket pool for the requests object, and created the requests object needed for the Adafruit IO connection.
This process can fail for various reasons, and is therefore included in a try
/except
block. If the process fails, the board will print the error and soft reload following a five second delay. The errors thrown by this process are sometimes very unclear. If you're receiving errors, check your SSID and password before continuing.
try: wifi.radio.connect(os.getenv("wifi_ssid"), os.getenv("wifi_password")) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) except Exception as error: print("Wifi connection failed.") reload_on_error(5, error)
Set Up Ping
Next is the ping IP set up and initial ping.
First, you'll set up the IP address that the code will be pinging, as defined when going through the code customisations. Next, you send out a ping. If the initial ping is unsuccessful, print a message to the serial console.
The initial_ping
variable is used later to determine whether to reload the board following a specific number of consecutive ping fails or not. If the ping fails, set it to False
. If the initial ping is successful, set it to True
.
ip_address = ipaddress.IPv4Address(PING_IP) wifi_ping = wifi.radio.ping(ip=ip_address) if wifi_ping is None: print("Setup test-ping failed.") initial_ping = False else: initial_ping = True
Connect to Adafruit IO and Retrieve the Time
First you connect to Adafruit IO using your credentials from your settings.toml file, and the requests
object created previously.
Next, you test the connection was successful by checking for the current time. The Adafruit IO set up and connection can fail for various reasons, but that failure isn't evident until you try to use it. Therefore, this time check is included in a try
/except
block. If the connection fails, it will print the error, and soft reload the board following a five second delay.
io = IO_HTTP(os.getenv("aio_username"), os.getenv("aio_key"), requests) try: sundial = io.receive_time() except Exception as error: print("Adafruit IO set up and/or time retrieval failed.") reload_on_error(5, error)
Variables
The last thing before the loop is initializing five variables for later use.
Time Tracking
There are four time-tracking variables.
-
check_time
- This is used to track the last time the code retrieves the time from Adafruit IO, which is then used to determine the next time the code should retrieve the time. It is initialized to0
so it can also be used to verify the first run though the code. -
network_down_time
- This is used to track the time, when the network is down, until the board should reset. This is only used when network down detection is disabled. -
ping_time
- This is used to track the last time a successful ping occurred, which is then used to determine the next time the code should send a ping. It is initialized to0
so it can also be used to verify the first run though the code. -
ping_fail_time
- This is used to track the last time a failed ping occurred, which is then used to determine the next time the code should send a ping.
ping_time = 0 check_time = 0 ping_fail_time = time.time()
Network Check
There is one check variable.
-
network_check
- This is used to determine whether to reset the board if the network is down and network down detection is disabled. It is initialized to1
because its code trigger state isNone
(the state set when the network check ping fails), and therefore, if it is never set toNone
(because the network ping check is successful), the resulting reset code is never run.
network_check = 1
Count Tracking
There is one count-tracking variable.
-
ping_fail_count
- This is used to keep track of the consecutive ping failures so the code knows when to blink or not blink.
ping_fail_count = 0
The Loop
The last section of code is inside the while True:
loop. This means it runs the first time the board starts up, and will continue looping until the board is reset.
while True: [...] # This is a way of indicating there is more code here. # It will be used again this section for this purpose.
Update the Current Time
The first line inside the loop sets the current time. This will happen every time the loop runs. current_time
is used throughout the code along with the other time-tracking variables to determine time intervals in a non-blocking manner.
current_time = time.time()
The try
and except
Block
The canary is designed to be plugged in and left alone. The majority of the code in the loop involves WiFi and Adafruit IO. Both of these can fail for various reasons, and if used standalone, the failures will cause the code to fail and stop running. This isn't conducive to a "set it and forget it" situation. So, to ensure the code keeps running, the rest of the code is wrapped in a try
/ except
block.
The way this works is that the code in the try
block will be run from top to bottom. If no errors are encountered, it will skip the except
block, and start again at the top of the loop. If an error is encountered, it will skip the rest of the code in the try
block, and immediately run the code in the except block.
The try
block contains all of the code meant for the loop that might run into an error.
The except
block is used to handle the encountered errors, known as exceptions. It contains the code that ensures everything continues to run. In this case, if an Exception
occurs, the code will use the reload_on_error()
function to print the error info to the serial console, wait ten seconds, and then hard reset the board. This will resolve the majority of errors that might occur, and allow the code to run fresh so it can continue on its way.
[...] try: [...] # Code in the try block is nested here. except Exception as error: reload_on_error(10, error, reload_type="reset")
Time to Light Up LEDs
This code runs when it is the first time running the code after start up, or if the time check interval has passed.
if not check_time or current_time - check_time >= TIME_CHECK_INTERVAL:
The Network Check Ping
If you run the Adafruit IO time check code when there is no network available, it will throw an exception, and in the case of this example, reset the board. That means the board would reset every 20 seconds or so, without running any of the other code in the try
block.
If network down detection is enabled, this means that the network down code will never run. That code would be skipped, and the canary will never blink to let you know your network is down.
To handle this eventuality, the code performs the network check ping before every time check. The time check code only runs if the network check ping was successful. This ensures that all of the following code in the try
block will be run, including the network down code.
network_check = wifi.radio.ping(ip=ip_address)
Receive the Current Time
If the network check ping is successful, the time code is run.
You reset check time to continue tracking and determine the next time the time code should be run.
check_time = time.time()
You check with Adafruit IO to receive the current time, and save it to the variable sundial
, which allows you to access specific pieces of the time, such as seconds or hours.
sundial = io.receive_time()
You print to the serial console a message including the date and time received from Adafruit IO, in a human-readable format.
This line is split into two parts because it is otherwise too long to adhere to code line length conventions. Due to how CircuitPython handles f-strings, you must explicitly "add" them together to concatenate them in the serial console. This is why there is a +
at the end of the first line.
print(f"LED color time-check. Date and time: {sundial.tm_year}-{sundial.tm_mon}-" + f"{sundial.tm_mday} {sundial.tm_hour}:{sundial.tm_min:02}")
Light It Up
This lights up all of the LEDs to the proper color based on the time, using the color_time
function. It lights up red or blue by default.
The function expects you to provide the current hour. You can use sundial
to easily access and provide the hour received from Adafruit IO. The color_time
function returns the appropriate color.
That color is provided to pixels.fill()
which lights up all of the NeoPixels the same color.
pixels.fill(color_time(sundial.tm_hour))
Network Check Ping Failure
Immediately after a failed network check ping, the code prints the following to the serial console.
print("Network check ping failed.")
If Network Down Detection is Disabled
Disabling the network down detection feature of your canary means the code will run the next block of code, and skip the rest of the code in the try
block.
Repeated Ping Failures
If the network check ping continues to fail for longer than the specified network down reload time interval, the code will print a message to the serial console, wait 3 seconds, and hard reset the board.
This if statement is split into two lines, as it exceeds code line length conventions if included on one line. To indicate to the code that the line continues on the next line, a \
is included at the end of the first line. This is standard for CircuitPython and Python.
if not NETWORK_DOWN_DETECTION and network_check is None and \ current_time - network_down_time > NETWORK_DOWN_RELOAD_TIME: print(f"Network check ping has failed for over {NETWORK_DOWN_RELOAD_TIME} seconds.") reload_on_error(3, reload_type="reset")
If Network Down Detection is Enabled
Leaving the network down detection feature of your canary enabled means the code will skip the previous block, and run everything else in the try
block.
Ping for Consistent Network Status Verification
This code runs when it is the first time running the code after start up, or if the up ping time interval has passed.
if not ping_time or current_time - ping_time >= UP_PING_INTERVAL:
Reset the ping time to continue tracking when to perform the next ping.
ping_time = time.time()
Send the ping to the specified IP address, and save the result to wifi_ping
to use in the rest of this section.
wifi_ping = wifi.radio.ping(ip=ip_address)
When the Ping is Successful
A successful ping means wifi_ping
has been assigned a value, and is therefore equal to something other than None
. Therefore the last section of code in this block is run.
Since the ping is successful, set the ping fail count to 0
. Print the IP address and the ping time to the serial console.
if wifi_ping is not None: ping_fail_count = 0 print(f"Pinging {ip_address}: {wifi_ping} ms")
When the Ping Fails
A failed ping means wifi_ping
is equal to None
. This section of code runs if a ping fails and one second has passed.
if wifi_ping is None and current_time - ping_fail_time >= 1:
Reset the ping fail time to continue tracking the next time to run this section of code.
ping_fail_time = time.time()
Add one to the ping fail count tracking, and print to the serial console how many times the ping has consecutively failed.
ping_fail_count += 1 print(f"Ping failed {ping_fail_count} times")
If the ping fail count exceeds the consecutive ping fail to blink value, begin blinking the canary. It blinks red by default. The blink duration is on for 0.5 seconds, then off for 0.5 seconds, which is to say, it blinks for half a second every second.
if ping_fail_count > CONSECUTIVE_PING_FAIL_TO_BLINK: blink(BLINK_COLOR)
If the set up ping was successful, the blinking will continue until the next successful ping resets the ping fail count.
If the set up ping failed, it means that the network connectivity was having issues from the beginning, and if it continues as such, a board reload may be needed to attempt to resolve the issue. If this situation occurs, and the ping fail count exceeds 30, the board will immediately soft reload.
if not initial_ping and ping_fail_count > 30: reload_on_error(0)
Text editor powered by tinymce.