There might come a time when you want a project running 24/7 permanently online.Β Weather and Real Time Clock (RTC) updates are common uses.Β It's not enough to code a request to an API, you must know how to avoid every possible scenario that might crash your script. That's where error exception handlers come in.
Learning how to use exception handlers is the most important aspect of an always online script.Β
If you're a beginner please read Networking in Circuit Python then Web API's & You first. 24/7 Online Requests is a follow up guide for intermediate level Circuit Python coders. Some parts of this guide infer you already know the basics for getting your board online & interacting with Web API's.
In Circuit Python error handlers are based on a try/except paradigm. You'll often hear programmers refer to it as try/throw/catch for catching errors but that's referencing other languages such as C++ or Java. That terminology is fine to use when discussing help with errors. Just know in Circuit Python the syntax is try/except for the purposes of online API scripts.
Basic WiFi Setup
Let's say we want to connect an Adafruit ESP32-S3 Feather to WiFi.
import os
import adafruit_connection_manager
import wifi
import adafruit_requests
# Initalize Wifi, Socket Pool, Request Session
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
SSL_CONTEXT = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
# adafruit_requests.Session keep outside the main loop
# otherwise you get Out of Socket errors.
requests = adafruit_requests.Session(pool, SSL_CONTEXT)
# Use settings.toml for credentials
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
# Connect to WiFi
wifi.radio.connect(ssid, password)
# Ensure your settings.toml has the following CIRCUITPY_WIFI_SSID = "YourSSID" CIRCUITPY_WIFI_PASSWORD = "YourPassword"
The above code is as basic as it gets for attempting to connect to WiFi but what happens if your WiFi access point is down or the credentials are wrong?Β Your script will cease to function and will crash with an error to a console.Β The error could be any number of possible errors such as out of sockets, out of retries, failure to get host, and so on.
Basics of Try/Except
By wrapping the WiFi connection request in a try/except, instead of crashing to REPL, it opens some important options which include (but not limited to):
- print the error to serial console
- ignore the error completely
- retry from where try was initiated
- reload code.py
- restart the microcontroller
try:
wifi.radio.connect(ssid, password)
except ConnectionError as e:
print(f"β Connection Error: {e}")
wifi.radio.connected
wifi.radio.connected is a function in the wifi library that knows if it is connected to a WiFi access point or not.Β It will return a boolean either True or False.Β We can use this "status" to know if we should attempt to reconnect or not anytime in the script.
By moving the WiFi connection attempt into the while True loop you can force it to continually check if it's connected... infinitely. The script will only continue past an exception if there is no error and a successful WiFi connection is made. In this example if you see the message
π‘ Wifi!
then the connection was successful and the script will continue.
while True:
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except ConnectionError as e:
print(f"β Connection Error: {e}")
print("π‘ Wifi!")
Exception Handling Types
There are many different error types. Sometimes they can be unique to a specific library. There are global error handlers built into Circuit Python such as:
- ConnectionError
- KeyError
- OSError
- RuntimeError
- ValueError
- and many more...
The full list of Circuit Python Exceptions are documented here.
It's perfectly fine to use multiple error handlers at the same time. For example:
while True:
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except (OSError, RuntimeError, ValueError, ConnectionError) as e:
print(f"β Error: {e}")
print("π‘ Wifi!")
Using every exception handler is a shotgun approach. The downside is it will not print the line the error spawned from or which error handler it came from. It will only print the error message. Sometimes that's a good thing, sometimes that's a bad thing.
Multiple Excepts
It is also possible to use multiple except statements with only 1 try.Β This is a way you can split up errors to be more specific which handler is being triggered even if it doesn't print the line number.
while True:
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except OSError as e:
print(f"β OSError: {e}")
except RuntimeError as e:
print(f"β RuntimeError: {e}")
except (ValueError, ConnectionError) as e:
print(f"β Value or Connection Error: {e}")
print("π‘ Wifi!")
Let's say in a hypothetical circumstance that you will never get a RuntimeError during wifi.radio.connect. If an error handler will never be triggered then it's pointless to add it to your except statement.Β You'll eventually figure out which error handlers are best suited for different situations. How? By running your script until it fails. Every time you encounter a new error you add it to the list of errors to check for. This helps keep your script more efficient because every character of code costs against RAM usage. The less code you write, the less RAM you use. If you have a board with a ton of RAM to spare then absolutely, sure, you can add every exception handler in the list. Some lower power boards that can barely run Circuit Python do not have much RAM overhead. Being efficient with your code is a great habit to cultivate.
Conditional Clauses
Clauses are used within a while, for, or try loop.Β Since an always online script will always be within a loop these can be used at any time. You will often see try/except used with conditional clauses.
- pass (does nothing and acts only as a placeholder while coding a new function)
- break (breaks to try or beginning of script)
- continue (continues to retry the try until successful)
For more information on conditional clauses please refer to Python Documentation Section 4.4
Here is an example use of break within an exception:
while True:
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except ConnectionError as e:
print(f"β Connection Error: {e}")
break # this will start the script over from the beginning
print("π‘ Wifi!")
Request Error Handling
Now let's say we've connected to WiFi without any errors. Next let's do a get request to an API.Β The SOURCE will be a url that you want to access, typically one that is formatted with a .json extension or file type.
SOURCE = "https://httpbin.org/get"
HEADER = {"user-agent": "blinka/1.0.0"}
while True:
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except (OSError, RuntimeError, ValueError, ConnectionError) as e:
print(f"β Error: {e}")
print("π‘ Wifi!")
while wifi.radio.connected:
try:
with requests.get(url=SOURCE, headers=HEADER) as response:
response_json = response.json()
except KeyError as e:
print(f"β Key Error: {e}")
Using a try/except around the request in the same manner as the wifi connection. If a GET request fails it has its own exception handler. Splitting up exception handlers allows you to make more modular code. Using print statements for debugging will show which section an error is being thrown from. With a GET request a KeyError is a json formatting error.Β
I'm walking you through each step of building up try/except layers intentionally so when you see the final script it won't be intimidating at all.Β
The small example above is straight forward to understand. If your script becomes 1000 lines long you will have many nested try/excepts.Β The most important try/except is the first one that encompasses everything from directly below while True and ends at the very bottom of the entire script, so no matter what happens (in theory) any error that might slip through other error handlers will be caught at the end, preventing a crash, and allowing your script to retry from the beginning... forever.
OSError errno
errno is a subset of the CPython implementation where you can catch very specifc error numbers from an error handler.Β More information is available in the Circuit Python Documentation on errno.
Thanks to Deshipu in a discord discussion for figuring out how to catch and handle gaierror -2 specifically.
while wifi.radio.connected:
try:
with requests.get(url=SOURCE, headers=HEADER) as response:
response_json = response.json()
except (OSError, KeyError) as e:
if e.errno == -2:
print(f"β gaierror (DNS failure), waiting to hard reset {e}")
time.sleep(240)
microcontroller.reset()
print(f"β Key Error {e}")
DNS Exception Handler (gaierror -2)
The OSError gaierror -2 is a quirky error in Circuit Python that stems from an upstream implementation of the ESP-IDF SDK. In my experience it can randomly occur when DNS resolution fails to resolve a host name. This is typically due to WiFi failing in the middle of a script because your router lost connection to the ISP, your DNS resolver failed, a failing ISP hop, or the host server is having an issue. You can catch the DNS error specifically using errno. The gaierror is an OSError you'll encounter seemingly randomly due to issues beyond your control.
The error message associated with gaierror -2 is:Β
- Name or service not known
My personal preference to combat a gaierror is to force the script to sleep for a period of time. This provides time for whatever external issue is causing it, to sort itself out. Then hard reset the controller just in case the issue is wifi.radio socket related. It will take a while for your board to recover and reconnect with this method but the point is that it will recover instead of crashing to console.
Traceback Module
Traceback is a more advanced debugging helper and core module that is customizable and beyond the scope of this playground note. Here is theΒ documentation on Traceback. If you become frustrated with exceptions (especially since they don't show the line # of the error) and want to print more verbose errors then look into the traceback module.
Request Example Script:
import os
import time
import microcontroller
import adafruit_connection_manager
import wifi
import adafruit_requests
# Initalize Wifi, Socket Pool, Request Session
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
SSL_CONTEXT = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
# adafruit_requests.Session keep outside the main loop
# otherwise you get Out of Socket errors.
requests = adafruit_requests.Session(pool, SSL_CONTEXT)
# Use settings.toml for credentials
ssid = os.getenv("CIRCUITPY_WIFI_SSID")
password = os.getenv("CIRCUITPY_WIFI_PASSWORD")
SOURCE = "https://httpbin.org/get"
HEADER = {"user-agent": "blinka/1.0.0"}
while True:
# Connect to Wi-Fi
print("\nConnecting to WiFi...")
while not wifi.radio.connected:
try:
wifi.radio.connect(ssid, password)
except ConnectionError as e:
print(f"β Connection Error: {e}")
time.sleep(60)
print("π‘ Wifi!")
while wifi.radio.connected:
try:
try:
with requests.get(url=SOURCE, headers=HEADER) as response:
response_json = response.json()
# Request error catcher. If script fails during request.
except (OSError, KeyError) as e:
if e.errno == -2:
print(f"β gaierror (DNS failure), waiting to hard reset {e}")
time.sleep(240)
microcontroller.reset()
print(f"β Key Error: {e}")
time.sleep(60)
# Your online API section goes here
response_headers = response.headers
print(f"Response Headers: {response_headers}")
# General error catcher. If script fails after a successful request.
except (OSError, ValueError, RuntimeError) as e:
print(f"β General Exception Error: {e}")
time.sleep(60)
break # breaks to while True to re-check WiFi status
time.sleep(300) # If everything succeeds, time until next poll update
This is a basic boilerplate that I use in most of my online API scripts to start with.Β All you have to do is provide the SOURCE, HEADER, and any API calls to the host in the relevant section.
Hopefully you now have a better understanding of what it takes to keep a script online 24/7 with Circuit Python.Β It's simply a matter of being able to control what happens when an error occurs so you can permanently prevent your script from crashing.
This page (24/7 Online Requests with CircuitPython) was last updated on August 25, 2024.
Text editor powered by tinymce.