Introduction:
If you have the need your WiFi project to operate at various locations with different WiFi SSID/PASSWORD settings at each location, read on. If you are using an MCU with built-in WiFi that CircuitPython 9.0.0 or later supports, there may be a solution to your issue.
Overview of the project:
This article will provide you with two tools to get you started.
- A code.py defined function (def) that will cycle through the WiFi networks defined in settings.toml. It will also show a sorted list of available WiFi access points found locally.
- A sample settings.toml to get you started.
- There are additional functions and features that will be covered as we go along.
- There will be a description of how each function works and interacts.
Why would you need multiple WiFi SSIDs in IoT projects?:
Let’s say you have a project that you must develop at home, show friends how it works at your bridge club, test it under various situations, and demonstrate its features to a customer. Having all the SSIDs and PASSWORDs predefined and having your project cycle through them without your intervention, except the first time you add them, would speed things up.
Prerequisites:
The system requirements are simple. An MCU with built-in WiFi that is supported by Circuit Python 9.0.0 or newer. My tests were run on a Raspberry Pi Pico W. All the libraries are built into CP 9.x.x that the sample code.py needs. They are: import os, wifi, random, binascii. The additional libraries used by the diagnostic code are import time, board, digitalio, ipaddress, supervisor, microcontroller and are also built-in.
If your MCU is listed at the web site below, your board is probably supported.
https://docs.circuitpython.org/en/latest/shared-bindings/wifi/index.html
Click on Available on these boards for a full listing. Considering the length of the boards listed, I have not tested, nor can I guarantee this code will work with any or all of them.
The settings.toml file.
The provided settings.toml file shows 9/10 SSID/PASSWORD pairs. I say 9/10 because the settings.toml file shows 9 pairs but calls for 10. I will describe each part of the WiFi configuration:
- WiFi SSID/PASSWORD pair. Each pair is defined with SSIDnumber and PASSWORDnumber. Each pair’s number must be between 0 and WIFI_ROUTERS setting; inclusive. The example shows 0 through 10. 9 is skipped and 10 has empty strings (“”), both for testing.
- WIFI_ROUTERS sets the number of SSID/PASSWORD pairs to be checked. (Remember 0 is the first)
- Example miscellaneous constants defined. All kinds of private information can be put in your settings.toml file. (AdaFruit Passwords, Credit Card Numbers, Birthdays, URLs, Addresses, and WiFi SSIDs and PASSWORDS.)
Sample settings.toml file:
# This file is where you keep secret settings, passwords, and tokens! # If you put them in the code you risk committing that info or sharing it # ssid = os.getenv("WIFI_SSID") # password = os.getenv("WIFI_PASSWORD")# # WIFI_SSID0 = "WiFiNetwork0" # nonexistant WIFI_PASSWORD0 = "password0" # WIFI_SSID1 = "TestNet" # dummy network WIFI_PASSWORD1 = "password1" # WIFI_SSID2 = "ATTByTheSea" # Aaron's Home WIFI_PASSWORD2 = "SheSellsSeaShells" # WIFI_SSID3 = "Aristotle" # Todd's Home WIFI_PASSWORD3 = "password3" # WIFI_SSID4 = "MyWiFi_12345678" # WIFI_PASSWORD4 = "password4" # WIFI_SSID5 = "YourWiFi_87654321" # Alternate WIFI_PASSWORD5 = "password5" # WIFI_SSID6 = "Verizon_STUVWXYZ" # Verizon WIFI_PASSWORD6 = "password6" # WIFI_SSID7 = "MyOfficeWiFi" # Ben's Home WIFI_PASSWORD7 = "GetToWork" # WIFI_SSID8 = "WhiteHouse" # Bruce's Home WIFI_PASSWORD8 = "Constitution" # #WIFI_SSID9 #WIFI_PASSWORD9 # WIFI_SSID10 = "" WIFI_PASSWORD10= "" # WIFI_ROUTERS = 10 # PING_ADDRESS = "8.8.8.8" # #CIRCUITPY_WIFI_SSID = "WiFiNetwork0" #CIRCUITPY_WIFI_PASSWORD = "password0" # # To auto-connect to Wi-Fi #CIRCUITPY_WIFI_SSID="your_wifi_ssid" #CIRCUITPY_WIFI_SSID #Wi-Fi SSID to auto-connect to even if user code is not running. #CIRCUITPY_WIFI_PASSWORD="your_pass_word" #CIRCUITPY_WIFI_PASSWORD #Wi-Fi password used to auto connect to CIRCUITPY_WIFI_SSID. # # To enable modifying files from the web. Change this also! # Leave the User field blank in the browser. CIRCUITPY_WEB_API_PASSWORD="passw0rd" CIRCUITPY_WEB_API_PORT=80 # ntp_server="time-b-g.nist.gov" location="Baltimore, US" LATITUDE="39.xxxxx" LONGITUDE="-76.xxxxx" Daylight_Saving_Time=True
Main Script (code.py):
I will go through the provided code.py file by line number(s). If you pull the code.py into Notepad, Notepad++, Visual Studio, or any text editor that shows line numbers the discussion will be easier. A *.pdf is available at the Forum using the above link.
Lines 1 – 3: Comments naming program code.py.
Lines 5 – 8: The required libraries that must be included. All are internal to CircuitPython 9.x.x. No libraries need to be copied into the lib/ folder.
Lines 17 – 49: def scan_sort_wifi(by=1, order=True):
This DEfined Function (def) Will retrieve, sort, and print the sorted list to REPL. To call the def you can put the sort option in the first position, and sort order in the second position. Sort Option: [0] Name, [1] Tx Power, [2] Channel, [3] No Sort. Sort Order True is small to large, False is large to small.
If you do not specify sort option or sort order, name and small to large will be selected. This def is for operator reference and diagnostic use. The most useful statistic is the relative power. As all the power values are negative; -40db has more power than -60db.
Lines 21 – 23: Set variables and print column headers on REPL.
Lines 25 – 34: Get a listing from the WiFi module of all the WiFi access points that are locally transmitting. The list is stored in variable aps[].
Lines 36 – 38: Sort the list in aps[] and store it in network2. The sort can be by Name, Relative Power, Channel or No Sort.
Lines 39 – 49: Print the sorted listing of WiFi access points that are available in the local area to REPL.
Lines 51 – 88: def connect_to_wifi():
This function will attempt to connect to the list of WiFi Access Points defined in the settings.toml file. It will cycle through the list until it finds success, and then will ping an IP address of your choice. The WAN side of you boundary router is a good choice or the WAN default gateway.
Lines 54 – 57: define which WiFi driver we will be using, and pick a random ptr for the access point to start with. A random start point removes any preference toward a particular access point. You can set ptr to a fixed number if you prefer to start with the same access point each time.
Lines 58 -75: Attempt to connect to the list of access points in settings.toml. If the connection fails, the next access point in the list will be tried. When the end of the list (WIFI_ROUTERS) is reached, the pointer (ptr) will be reset to the top of the list (0). The progress is reported on REPL.
Lines 76 – 88: Statistics of the successful connection are displayed on REPL in this section of code. The display will contain IP Address, IP Subnet Mask, Subnet Default Gateway, Subnet Domain Name System, Device HostName, Radiated Power, and MAC Address.
Lines 93 – 98: Example on how to enable the WiFi device, and how to call scan_sort_wifi(1, True).
Lines 104 – 104: Example on how to set the Transmit Power.
Lines 105 – 105: Example on how to set an optional HostName, and how to call connect_to_wifi().
Lines 109 – 139: My diagnostic code that connects to a WiFi access point for about 10 seconds then reboots the MCU to run all the code again.
Lines 112 – 117: Sets the includes and enables the Green LED.
Lines 119 – 125: Sets the includes and pings a designated address to test the WiFi connection. The WAN side of your router is a good test.
Lines 126 – 139: Tests if we are still connected and shows status.
Line 128 tests if we are still connected. If we are not, say so, if we have disconnected, reload with supervisor.reload(). If you wish, comment-out line 131 and a full reset will happen. This will most likely disconnect you from REPL. Also after 10 loops, the code is reset and we test again.
Lines 140 – 141: We should never get here, but if we do we want to know it.
Sample code.py file:
####################################### # Multiple WiFi Networks with CP 9.x.x ####################################### import os import wifi import random import binascii ####################################### # Diagnostic ####################################### print('\f') print('\t*** Program Start ***') print('') ####################################### def scan_sort_wifi(sort_by=1, order=True): ####################################### _ = 0 print("\tAvailable CYW43439KUBG WiFi networks:") print(" Seq\tSSID\t\t\t\b\b\bPower Channel") access_points = [] # Scan for WiFi networks for networks in (wifi.radio.start_scanning_networks()): s = "."*(20-len(str(networks.ssid, "utf-8"))) if (len(s)==0): s = "*Hidden_SSID*" access_points.append((s, networks.rssi, networks.channel)) else: access_points.append((networks.ssid, networks.rssi, networks.channel)) _ += 1 #print("%3d\t%s%s %d %2d" % (_, str(networks.ssid, "utf-8"), s, networks.rssi, networks.channel)) wifi.radio.stop_scanning_networks() # Sort the found networks Sort by [0]=name, [1]=power, [2]=channel, [3]=No Sort if sort_by < 3: sorted_networks = (sorted(access_points, key=lambda _: _[sort_by], reverse=order)) else: sorted_networks = access_points # Unsorted found networks print('') # Print the list to REPL for _ in range(len(sorted_networks)): s = "."*(20-len(str(sorted_networks[_][0], "utf-8"))) print("%3d\t%s %s %d %2d" % ( _+1, str(sorted_networks[_][0], "utf-8"), s, sorted_networks[_][1], sorted_networks[_][2])) print('') ####################################### def connect_to_wifi(): ####################################### # Pick an access point at random ptr = random.randint(0, os.getenv("WIFI_ROUTERS")) # Attempt to connect to the WiFi networks in settings.toml while not wifi.radio.connected: # Loop while not connected try: SSID, PASSWORD = os.getenv("WIFI_SSID"+str(ptr)), os.getenv("WIFI_PASSWORD"+str(ptr)) except: SSID, PASSWORD = 'Null', 'Null' if(SSID == None) or (PASSWORD == None): SSID, PASSWORD = 'Nope', 'Nope' s = 30-(len(SSID) + len(PASSWORD)) if ptr < 10: print(' ', end='') print(ptr, SSID, PASSWORD, '.'*s, end=' ') try: print('WiFi Connecting.', end='') wifi.radio.connect(SSID, PASSWORD) except: ptr += 1 ptr = (ptr % os.getenv("WIFI_ROUTERS")) print('\b\b\bon Failed.') print('\b\b\b\bed, Hurray!') # We are connected IP = wifi.radio.ipv4_address Mask = wifi.radio.ipv4_subnet Gateway = wifi.radio.ipv4_gateway DNS = wifi.radio.ipv4_dns hostname= wifi.radio.hostname power = wifi.radio.tx_power print("\n\t\tWiFi Connection Information:\n SSID:", SSID, ' ~ IP:', IP, ' ~ Mask:', Mask, ' ~') print('\tGateway:', Gateway, ' ~ DNS:', DNS, ' ~ HostName:', wifi.radio.hostname, ' ~') print('\tRadiated Power: '+str(power)+'mw ~ MAC addr:', end=' ') res = (binascii.hexlify(bytearray(wifi.radio.mac_address))) for _ in range(0, len(res), 2): print(str(res[_:_+2])[2:4].upper(), end=':') print('\b \n') # Cleanup ####################################### # Start of Your code ####################################### wifi.radio.enabled = True ####################################### # Scan and show the available WiFi networks, Sort Optional ####################################### scan_sort_wifi(1, True) # Sort by | 0>name, False | 1>power, True | 2>channel, False, | 3> No Sort ####################################### # Attempt to connect ####################################### # Optional unique HostName #wifi.radio.tx_power = 63.0 # 1 through 63, 31.75 default #wifi.radio.hostname = "MultiWiFi" # Lets go connect to WiFi connect_to_wifi() ####################################### # Diagnostics ####################################### import time import board import digitalio GreenLED = digitalio.DigitalInOut(board.LED) GreenLED.direction = digitalio.Direction.OUTPUT GreenLED.value = True import ipaddress import supervisor import microcontroller addr = "71.179.161.1"#"192.168.12.1" ip1 = ipaddress.ip_address(addr) print("pinging", ip1)#, end=' ') print("ping round trip time:", wifi.radio.ping(ip1), "Seconds") t = time.time() while True: GreenLED.value = not GreenLED.value if (wifi.radio.connected and ((t+10) > time.time())): print(' ', time.time(), '\tWe are Good ', end='\r') time.sleep(1) else: print('\n\t\aTimeout/Lost Connection\a') time.sleep(1) supervisor.reload() # Like Ctrl-C, Ctrl-D microcontroller.reset() # Like RESET button or remove/connect power print('') print('\t*** Program Finis ***') ''' >>> import wifi >>> dir(wifi) ['__class__', '__init__', '__name__', 'AuthMode', 'Monitor', 'Network', 'Packet', 'Radio', 'Station', '__dict__', 'radio'] >>> dir(wifi.radio) ['__class__', 'ap_active', 'connect', 'connected', 'enabled', 'hostname', 'ipv4_address', 'ipv4_address_ap', 'ipv4_dns', 'ipv4_gateway', 'ipv4_gateway_ap', 'ipv4_subnet', 'ipv4_subnet_ap', 'mac_address', 'mac_address_ap', 'ping', 'set_ipv4_address', 'set_ipv4_address_ap', 'start_ap', 'start_dhcp', 'start_dhcp_ap', 'start_scanning_networks', 'start_station', 'stations_ap', 'stop_ap', 'stop_dhcp', 'stop_dhcp_ap', 'stop_scanning_networks', 'stop_station', 'tx_power'] >>> '''
Upload the code and configuration:
To test if this program will work on your MCU (listed above), load CircuitPython 9.0.0 or newer, copy the code.py and settings.toml files to your board. Then startup REPL and see what is happening. It should cycle through the 10 SSID/PASSWORD pairs preloaded into settings.toml and fail at each one and move on to the next.
If you get wifi not found/defined, this program will not work because CP 9.x.x does not support your board. The AdaFruit Forum experts will have to address this.
Replace one of the test sites listed in settings.toml with your local WiFi SSID and PASSWORD then watch what happens. The program should pick a random site and cycle through to your site and connect. It will wait 10 seconds then reboot and pick a random site and cycle again. Then change the IP address in line 116 and verify that your MCU can ping a known good local site.
This should work out-of-the-box. If yours doesn’t, contact me on the AdaFruit Forum, @blakebr.
Conclusion:
I hope this utility helps you in making your programs more bulletproof while connecting to WiFi. I am looking forward to your real-life testing and improving this utility with your input.
I suggest you place the code snippet below someplace in your program to periodically test that the WiFi connection is still good.
if (wifi.radio.connected): # We are good, no action. time.sleep(0.01) else: print('aTimeout/Lost Connection\a') time.sleep(1) supervisor.reload() # Like Ctrl-C, Ctrl-D # Comment out to RESET microcontroller.reset() # Like RESET button or remove/connect power
Potential extensions:
An adventuresome coder may want to tackle taking the access point list sorted by relative power and comparing it to the SSID/PASSWORD listing and then starting the connection attempts with the known access point with the highest power and working through the list until success.