ESP8266 – Switch automatically between 2 access points
Overview
Sometimes it is necessary to switch the ESP8266’s connection between two WiFi access points. For example if you have a second AP in your house as a backup if your main AP fails.
In my case I stumbled over a connectivity problem with my ESP8266 devices. On my lot I needed to have 2 WiFi access points, because the WiFi modem/router I got from my ISP has a really lousy WiFi range (internal antenna). So I added a second WiFi access point in my backyard to be able to connect the devices located there to the same network. Some of the devices are on the range border of both AP’s and one or the other AP disappears in the ESP8266 frequently.
So I worked on a solution that allows the ESP8266 to detect the loss of WiFi connection and switch to the other AP automatically.
Luckily the ESP8266-Android framework has build in callbacks for WiFi events like ‘connected‘ ‘IP assigned‘ and ‘connection lost‘. In this small tutorial I show how to use this callbacks and how to switch between two known access points.
Development environment:
I use Visual Studio Code with PlatformIO. But if you prefer ArduinoIDE it is not difficult to transfer the code from the .cpp files into a single .ino file.
Sources
The complete code is available in my Bitbucket repository.
The tutorial code does not need any additional libraries beside of what is in the ESP8266 framework.
For easier maintenance the code is split into several header and code files
- wifiLib.cpp and wifiLib.h contains all functions and definitions for the WiFi connection, detection of disconnection and acquiring a list of available AP’s
- main.cpp and main.h contains the setup() and loop() functions
- ledLib.cpp and ledLib.h contains all functions to control some optical feedback by lighting or flashing the onboard LED’s. I am using Adafruit ESP8266 HUZZAH boards, which have two onboard LED’s, a red one (I call it activity LED) and a blue one (I call it the communication LED). If you have a different board or want to use other GPIO’s to signal the status, you need to adapt these files.
Check for available networks
Very first step before connecting to the primary or secondary AP is to check if the networks are available. The results are in the global variable usePrimAP. If true, the first AP we try to connect to is the primary AP, if false the secondary AP is used. The variable canSwitchAP indicates if both AP’s were found. If false only one of the defined AP’s was found, so there is no possibility to switch.
The scan of WiFi networks is done with the WiFi library function WiFi.scanNetworks(). After we got the list of networks we check if the defined primary and secondary AP’s are found. The result is saved in the variables bool foundPrim which tells us if the defined primary AP is found and byte foundAP which tells if one or both defined AP’s were found
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
doubleLedFlashStart(0.25); int apNum = WiFi.scanNetworks(); if (apNum == 0) { Serial.println("Found no networks?????"); doubleLedFlashStop(); return false; } byte foundAP = 0; bool foundPrim = false; for (int index=0; index<apNum; index++) { String ssid = WiFi.SSID(index); Serial.println("Found AP: " + ssid + " RSSI: " + WiFi.RSSI(index)); if (!strcmp((const char*) &ssid[0], ssidPrim)) { foundAP++; foundPrim = true; rssiPrim = WiFi.RSSI(index); } if (!strcmp((const char*) &ssid[0], ssidSec)) { foundAP++; rssiSec = WiFi.RSSI(index); } } |
In the next step we check the conditions. If we didn’t find any of the defined AP’s an error is returned.
1 2 3 4 |
switch (foundAP) { case 0: result = false; break; |
If we found only the primary AP or the secondary AP the condition is stored in the variables usePrimAP and canSwitchAP.
1 2 3 4 5 6 7 8 9 |
case 1: if (foundPrim) { usePrimAP = true; } else { usePrimAP = false; } canSwitchAP = false; result = true; break; |
If we found both primary and secondary AP we check which of the AP’s has the stronger signal. The first connection will use the AP with the stronger signal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
default: if (rssiPrim < rssiSec) { usePrimAP = true; // RSSI of primary network is better } else { usePrimAP = false; // RSSI of secondary network is better } canSwitchAP = true; result = true; break; } doubleLedFlashStop(); Serial.println("WiFi scan finished after " + String((millis()-wifiConnectStart)/1000) + "s"); return result; } |
During the WiFi scan the two LED’s blink fast, but as the whole scan takes not more than 2 seconds, this is barely visible.
How to get a notification if the WiFi connection changes
The ESP8266WiFi library provides 3 callback functions for the WiFi station mode that can be used to call your own code in case
- the connection gets lost – WiFiEventStationModeDisconnected
- a connection is established – WiFiEventStationModeConnected
- an IP address was assigned – WiFiEventStationModeGotIP
The use of these callback functions is quite easy. First we need to declare them
1 2 3 4 |
/** Event handlers for WiFi */ WiFiEventHandler connectedEventHandler; WiFiEventHandler disconnectedEventHandler; WiFiEventHandler gotIPEventHandler; |
Then we need to tell the WiFi library which functions should be assigned to the callbacks This is done in the function connectInit()
1 2 3 4 5 6 7 8 9 |
/** connectInit Initiates connection */ void connectInit() { // Initialize the callback handlers connectedEventHandler = WiFi.onStationModeConnected(&gotCon); disconnectedEventHandler = WiFi.onStationModeDisconnected(&lostCon); gotIPEventHandler = WiFi.onStationModeGotIP(&gotIP); |
After initializing this function scans for the available AP’s and then calls the function which starts the connection.
1 2 3 4 5 6 |
// Scan for available WiFi networks scanWiFi(); // Try to connect to a WiFi network connectWiFi(); } |
To handle the WiFi connection/disconnection/re-connection sequences I defined 4 state which are stored in the variable connStatus. This variable is defined to start in the status CON_LOST, which here means as well that there was never a WiFi connection established
1 2 3 4 5 6 7 8 |
/** Connection status */ #define CON_INIT 0 // connection initialized #define CON_START 1 // connecting #define CON_GOTIP 2 // connected with IP #define CON_LOST 3 // disconnected /** Connection status */ byte connStatus = CON_LOST; |
This connStatus is handled by the function checkWiFiStatus, which I will explain later. Here are the three callback functions:
lostCon() is called when WiFi got disconnected
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** lostCon called when WiFi got disconnected */ void lostCon(const WiFiEventStationModeDisconnected& evt) { if (connStatus == CON_GOTIP) { Serial.println("Lost connection"); connStatus = CON_LOST; // Switch on both LED's to show disconnect status comLedFlashStop(); digitalWrite(actLED, LOW); digitalWrite(comLED, LOW); } } |
As you can see there is not much happening here. First we check if the last status was CON_GOTIP or CON_START. That tells us that we had a WiFi connection established already or tried to connect and that we lost this connection.
If this is the case, we send a short debug message over the serial port and switch the connection status to CON_LOST, which indicates the loss of a connection. For visual confirmation of this disconnected state I switch on both LED’s permanently.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** gotCon called when connection to AP was successfull */ void gotCon(const WiFiEventStationModeConnected& evt) { if (connStatus == CON_INIT) { Serial.println("Got connection after " + String((millis()-wifiConnectStart)/1000) + "s"); Serial.println("SSID: " + String(evt.ssid)); Serial.println("Channel: " + String(evt.channel)); Serial.println("RSSI: " + String(WiFi.RSSI())); connStatus = CON_START; // Switch LED status to connected but no IP comLedFlashStop(); digitalWrite(actLED, HIGH); digitalWrite(comLED, LOW); } } |
Here we check if the previous status was CON_INIT, which means that we try to connect to an AP. If this was the case, the connStatus is set to CON_START, some debug information is sent over serial and to show this status visually the activity LED is switched off and only the communication LED stays on.
And last, the gotIP() function. This is called when the ESP8266 got an IP address assigned by the AP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** gotIP called when IP address was assigned */ void gotIP(const WiFiEventStationModeGotIP& evt) { if (connStatus == CON_START) { Serial.println("Got IP after " + String((millis()-wifiConnectStart)/1000) + "s"); connStatus = CON_GOTIP; Serial.println(WiFi.localIP()); Serial.println("RSSI: " + String(WiFi.RSSI())); // Switch LED status to blinking comLED digitalWrite(actLED, HIGH); digitalWrite(comLED, HIGH); comLedFlashStart(0.5); } } |
Again, there is not much happening inside. We check if the last status was CON_START, which shows that we are trying to connect to an AP. If this is the case, some debug messages are sent and the status is set to CON_GOTIP, which means that the connection can be used now. And to show the status, the activity LED is turned off and the communication LED is starting to blink with a 0.5s frequency to show that the ESP8266 has established a working connection.
Connection Status callbacks
We have defined the callbacks that show the different status of the connection process. Here is the code that checks the different status and reacts to them. All of this is done in the function checkWiFiStatus.
The first check is if the initiating of the connection takes too long. On my network the connection including assignment of the IP address takes usually not more than 5 seconds. So the first check is we are waiting for a connection more than 10 seconds. If this is the case we initiate the connection to the alternative WiFi AP if possible or retry on the same AP.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** checkWiFiStatus Check current WiFi status and try to reconnect if necessary */ void checkWiFiStatus() { // Check if connection initialization was successfull if ((connStatus == CON_INIT) || (connStatus == CON_START)) { if ((millis() - wifiConnectStart) > 10000) { Serial.println("WiFi connection failed for more than 10 seconds"); Serial.println(WiFi.localIP()); // Toggle WiFi AP usePrimAP = !usePrimAP; connectWiFi(); } } |
Next check is if we lost the connection. If this is the case we try to connect to the alternative AP if possible or retry on the same AP
1 2 3 4 5 6 7 8 9 10 |
// Check if connection was lost if (connStatus == CON_LOST) { Serial.println("WiFi connection lost"); // Toggle WiFi AP if we can if (canSwitchAP) { usePrimAP = !usePrimAP; } connectWiFi(); } } |
Two more functions left for the WiFi stuff.
The first one is connectWiFi() which will start the connection to one of the two AP’s.
First WiFi.disconnect() is called. This looks awkward, but is necessary if an unsuccessful connection attempt is still ongoing. Then the WiFi is put into STA mode which means we attempt to connect to an AP. We disable automatic reconnecting here, as this code is handling the re-connection by itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** connectWiFI starts a connection */ void connectWiFi() { // Switch on both LED's digitalWrite(actLED, LOW); digitalWrite(comLED, LOW); // Connect to Wifi. WiFi.disconnect(); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(false); |
The next part set the status to CON_INIT to indicate that we started a connection attempt. Then it checks to which of the defined AP’s the connection should be established and then starts the connection attempt with WiFi.begin().
1 2 3 4 5 6 7 8 9 10 11 12 |
connStatus = CON_INIT; wifiConnectStart = millis(); Serial.println(); Serial.print("Start connection to "); if (usePrimAP) { Serial.println(ssidPrim); WiFi.begin(ssidPrim, pwPrim); } else { Serial.println(ssidSec); WiFi.begin(ssidSec, pwSec); } } |
The last function is initOTA() which prepares the device for over-the-air updates. This is quite standard stuff. There are only 2 parts I want to highlight here.
The first is the creation of a unique mDNS name for the OTA update. I use a part of the devices MAC address to create a unique name for the device.
1 2 3 4 5 6 7 8 9 10 11 |
// Create device ID from MAC address char hostApName[] = "MHC-xxxxxxxx"; String macAddress = WiFi.macAddress(); hostApName[4] = macAddress[0]; hostApName[5] = macAddress[1]; hostApName[6] = macAddress[9]; hostApName[7] = macAddress[10]; hostApName[8] = macAddress[12]; hostApName[9] = macAddress[13]; hostApName[10] = macAddress[15]; hostApName[11] = macAddress[16]; |
The second one is that I add the board name to the mDNS service text to make it easier to see what type of device it is
1 |
MDNS.addServiceTxt("arduino", "tcp", "board", "ESP8266"); |
You can either check the complete code here or just jump to the next page to the setup() and loop() functions of the tutorial.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/** initOTA initializes OTA updates */ void initOTA() { // Create device ID from MAC address char hostApName[] = "MHC-xxxxxxxx"; String macAddress = WiFi.macAddress(); hostApName[4] = macAddress[0]; hostApName[5] = macAddress[1]; hostApName[6] = macAddress[9]; hostApName[7] = macAddress[10]; hostApName[8] = macAddress[12]; hostApName[9] = macAddress[13]; hostApName[10] = macAddress[15]; hostApName[11] = macAddress[16]; ArduinoOTA.setHostname(hostApName); Serial.println("OTA host name: "+ String(hostApName)); ArduinoOTA.onStart([]() { Serial.println("OTA start"); digitalWrite(comLED, LOW); // Turn on blue LED digitalWrite(actLED, HIGH); // Turn off red LED }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { digitalWrite(comLED, !digitalRead(comLED)); // Toggle blue LED digitalWrite(actLED, !digitalRead(actLED)); // Toggle red LED }); ArduinoOTA.onError([](ota_error_t error) { digitalWrite(comLED, LOW); // Turn on blue LED digitalWrite(actLED, LOW); // Turn on red LED }); ArduinoOTA.onEnd([]() { digitalWrite(comLED, HIGH); // Turn off blue LED digitalWrite(actLED, HIGH); // Turn off red LED }); // Start OTA server. ArduinoOTA.begin(); MDNS.addServiceTxt("arduino", "tcp", "board", "ESP8266"); } |
And then there is one file I am not sharing on the Bitbucket repository because in this file I define the two AP’s and their passwords. As the file contains my network credentials, I do not share it with you.
You have to create this file or make the definitions inside the wifiLib.h header file. The structure of the definitions is like
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef Wifi_Lib_Private_h #define Wifi_lib_Private_h /** SSIDs of local WiFi networks */ static const char* ssidPrim = "PRIMARY_AP_NAME"; static const char* ssidSec = "SECONDARY_AP_NAME"; /** Password for local WiFi network */ static const char* pwPrim = "PRIMARY_AP_PASSWORD"; static const char* pwSec = "SECONDARY_AP_PASSWORD"; #endif // Wifi_lib_Private_h |
You need to replace PRIMARY_AP_NAME, SECONDARY_AP_NAME, PRIMARY_AP_PASSWORD and SECONDARY_AP_PASSWORD with the values for your setup.
setup() and loop() functions
The setup() and loop() functions are quite simple.
In setup() we just initialize the LED’s (see next page for more info), the serial connection and then connectInit() is called. As explained before, connectInit() checks the available AP’s and initiates a connection.
1 2 3 4 5 |
void setup() { initLeds(); Serial.begin(115200); connectInit(); } |
For the loop() function one additional flag is needed: bool otaInitDone. This flag shows if the OTA (over the air update) functionality is already initialized. This can be done only after the WiFi connection was established.
Inside loop() we check the flag otaInitDone and call initOTA() if required. Next we check if an OTA update request was received. And last we call checkWiFiStatus() to see if a WiFi connection is available or if the connection was interrupted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Flag if OTA was initiated bool otaInitDone = false; void loop() { if (!otaInitDone) { if (connStatus == CON_GOTIP) { initOTA(); otaInitDone = true; } } // Handle OTA requests ArduinoOTA.handle(); // Check WiFi status checkWiFiStatus(); } |
Here is an example log of the ESP8266 switching between the two AP’s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
Found AP: MyLTE RSSI: -86 Found AP: MHC RSSI: -66 Found AP: MHC2 RSSI: -70 Start connection to MHC Got connection after 3s SSID: MHC Channel: 7 RSSI: -66 Got IP after 4s 192.168.0.104 RSSI: -67 OTA host name: MHC-Lan-5C015A5D Lost connection WiFi connection lost Start connection to MHC2 Got connection after 3s SSID: MHC2 Channel: 13 RSSI: -50 Got IP after 4s 192.168.0.104 RSSI: -52 Lost connection WiFi connection lost Start connection to MHC Got connection after 3s SSID: MHC Channel: 7 RSSI: -81 Got IP after 4s 192.168.0.104 RSSI: -90 Lost connection WiFi connection lost Start connection to MHC2 Got connection after 3s SSID: MHC2 Channel: 13 RSSI: -49 Got IP after 4s 192.168.0.104 RSSI: -45 |
LED control
To control the LED’s I wrote a kind of small library with standard functions to let the LED’s flash in different frequencies. In this tutorial I do not use all of the functions, but I thought it might be interesting for you.
To flash the LED’s I use three different Tickers. Ticker is a standard library on the ESP8266 that makes it easy to have specific functions repeated frequently or just once after a defined time (But that’s part of another tutorial).
First we define the three Ticker and the default GPIO’s for the blue and red LED of my Adafruit ESP8266 HUZZAH boards.
1 2 3 4 5 6 7 8 9 10 11 |
/** Timer for flashing red detection/alarm/activity LED */ Ticker actFlasher; /** Timer for flashing blue communication LED */ Ticker comFlasher; /** Timer for flashing both blue and red leds */ Ticker doubleFlasher; /** Port for blue LED */ uint8_t comLED = 2; /** Port for red LED */ uint8_t actLED = 0; |
The function initLeds() can be called either without parameters, then the default GPIO’s are used or with the GPIO numbers for the two LED’s in case they are not on the Adafruit HUZZAH standard. This achieved by declaring the function as
1 |
void initLeds(uint8_t reqComLED = 2, uint8_t reqActLED = 0); |
initLeds() then intializes the GPIO’s as output and switches off both LED’s.
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Initialize LED pins */ void initLeds(uint8_t reqComLED, uint8_t reqActLED) { comLED = reqComLED; actLED = reqActLED; pinMode(comLED, OUTPUT); // Communication LED blue pinMode(actLED, OUTPUT); // Communication LED red digitalWrite(comLED, HIGH); // Turn off blue LED digitalWrite(actLED, HIGH); // Turn off red LED } |
The next three functions starts flashing of either one LED or both LED’s with opposite on/off status. The functions are called with a parameter that defines the on and off time in seconds. For example to flash the red LED with 1Hz frequency the call would be actLedFlashStart(0.5). Inside the functions the Ticker is activated by attaching a function to the Ticker and defining the time it should be called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * Start flashing of red led */ void actLedFlashStart(float flashTime) { actFlasher.attach(flashTime, actLedFlash); } /** * Start flashing of blue led */ void comLedFlashStart(float flashTime) { comFlasher.attach(flashTime, comLedFlash); } /** * Start flashing of both led */ void doubleLedFlashStart(float flashTime) { digitalWrite(actLED, LOW); // Turn on red LED digitalWrite(comLED, HIGH); // Turn off blue LED doubleFlasher.attach(flashTime, doubleLedFlash); } |
The functions attached to the Ticker only toggles the LED’s from on to off and back
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * Change status of red led on each call * called by Ticker actFlasher */ void actLedFlash() { int state = digitalRead(actLED); digitalWrite(actLED, !state); } /** * Change status of blue led on each call * called by Ticker comFlasher */ void comLedFlash() { int state = digitalRead(comLED); digitalWrite(comLED, !state); } /** * Change status of blue led on each call * called by Ticker comFlasher */ void doubleLedFlash() { int state = digitalRead(comLED); digitalWrite(comLED, !state); digitalWrite(actLED, state); } |
And last not least there are functions to stop the flashing. These functions switch off the LED and detach the toggle function from the Ticker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * Start flashing of red led */ void actLedFlashStop() { digitalWrite(actLED, HIGH); // Turn off red LED actFlasher.detach(); } /** * Start flashing of blue led */ void comLedFlashStop() { digitalWrite(comLED, HIGH); // Turn off blue LED comFlasher.detach(); } /** * Start flashing of both led */ void doubleLedFlashStop() { digitalWrite(actLED, HIGH); // Turn off red LED digitalWrite(comLED, HIGH); // Turn off blue LED doubleFlasher.detach(); } |
That’s all folks. You can find the whole source code on my Bitbucket repository.







15:52
Nice code, it really works perfectly after a small correction in the scanNetworks section, line 15 and 20.
It should read:
if (strcmp((const char*) &ssid[0], ssidPrim) == 0)
if (strcmp((const char*) &ssid[0], ssidSec) == 0)
The strcmp() funtion returns zero when the strings match.
Without the correction, the function finds all networks except the own ones, which results in a random connection to either of the two defined networks.
07:36
Sorry for the late reply.
Thanks for pointing to this mistake. I corrected it.