Wifimanager: background sta connect, reconnect to saved ap on power failure

Created on 22 Aug 2018  路  22Comments  路  Source: tzapu/WiFiManager

There is an issue with short lapses in connectivity and especially power failures that need a special handling to deal with either by the user or the library.

Ideally we would fire up the softap and leave sta on to connect, if status is connected we can abort the configportal automatically or after a timeout.

The problem with this is having a non connected sta makes softap unstable if not completely unusable. The theory is that the channel scanning autoconnect does makes the softap drop clients.

With this in mind there are a few options to resolve this.

Library

  • leave sta+ap mode and hope for the best...
  • startap with wifi.channel to make the channel static, might increase stability.
  • stop sta, start ap, do background scans for ap, if default ap is found in scan, restart sta snd try to connect, abort softap on result.
  • if no softap clients switch to sta and try to reconnect back, rinse repeat

User

  • use a long connecttimeout and a short configportaltimeout, on exit, if still not connected reboot
  • let user do it themselves, do not use autoconnect, use user code to check and timeout after power failure determined via reset code and handle accordingly start/stop configportal as needed.

It depends on user case, but If there is a saved credential, the odds of a power failure is higher than reconfigure of credentials. So waiting a minute for the router before starting configportal is a practical solution, also using an ondemand configportal removes this issue entirely.

Another options is to use some state tracking, if the last reboot was pwr, and last status was connected and time since last reboot is x, odds are router is rebooting...

A good option might be to fire a callout if saved ap found while cp is running.. and let user do whatever, stopconfigportal or restart esp.

Discussion enhancement

All 22 comments

added several private options for testing this

    bool          _disableSTA             = false; // disable sta when starting ap, always
    bool          _disableSTAConn         = true;  // disable sta when starting ap, if sta is not connected ( stability )
    bool          _channelSync            = false; // use wifi channel when starting ap

I am encountering the same issue.

The current code seems to do something very close to the desired functionality, except it leaves the STA up while also serving the soft AP, which seems to interfere with the next attempt to run the chip in STA mode. Would love to see a solution to this that follows the description above:
"[when attempting manual resolution] stop sta, start ap, do background scans for ap, if default ap is found in scan, restart sta snd try to connect, abort softap on result."

In a nutshell, the desire is for an ReconnectWiFi function on the WifiManager, which attempts three steps, in sequence:

  1. If (WiFi.status()==WL_CONNECTED), all is good - return true. Otherwise, proceed to 1
  2. Attempt to connect with saved ssid/password, if available (AP is off, STA is on), until _connectTimeout expires or (WiFi.status()==WL_CONNECTED). If (WiFi.status()==WL_CONNECTED), all is good - return true. Otherwise, or if no saved ssid/password pair is available, proceed to 2.
  3. Let user manually configure WiFi (AP is on, STA is off), until _configPortalTimeout expires or WiFi.status() is WL_CONNECTED. Return (WiFi.status()==WL_CONNECTED)

The user could then write this in their loop() function:
void loop() {
if (wifiManager.ReconnectWiFi(AP_SSID, AP_PWD)) {
// WiFi connection is ready for use.
}
}

This would allow very robust self-healing (by repeating step 1) as well as manual healing (by repeating step 2) any time an interruption occurs, be it transient (power outage, AP reboot, etc.), or permanent (SSID change, password change, security mode change, etc.)

Happy to help test a solution through:

  1. Simulated power cycles of the AP (disabling and re-enabling the wireless network)
  2. SSID changes
  3. Password changes
  4. Security scheme changes.

Thanks!

Cheers,
~alex

that is literally what I posted above...

all those features can be done now by user code, or autoconnect without cp

Except the background scan for home ap, this might be in the next version, but there are ways to do it now already

Thank you for the quick reply, Shawn!

I feel like I'm missing something... I wrote the trivially simple sketch below... I let the board connect to WiFi (with saved credentials), then I turned off the wireless network. Predictably, after a few seconds, the board lost connectivity and started serving the captive portal. I then turned the wireless network back on and, although the board continued to cycle between trying to connect to WiFi and serving the captive portal, it was never able to reconnect to WiFi. When I physically reset the board, it connected to WiFi in a couple of seconds.

What is your recommendation for changing the sketch below without: 1. Losing most of the benefits of using the WiFiManager class and 2. Repeatedly powercycling the board (can't be good for it over the long term...)

include

//needed for library

include

include

include

include

void setup() {
}

const unsigned long CONNECT_TIMEOUT = 180; // Wait 3 minutes to connect to the real AP before trying to boot the local AP
const unsigned long AP_TIMEOUT = 180; // Wait 3 minutes in the config portal before trying again the original WiFi creds
const char* AP_SSID = "ARDUINO"; // Create an access point with this SSID
const char* AP_PWD = "CONFIGURE"; // Protected the access point with this password

bool EnsureWiFi() {
if (WiFi.status() != WL_CONNECTED) {
WiFiManager wifiManager;

wifiManager.setConnectTimeout(CONNECT_TIMEOUT);
wifiManager.setTimeout(AP_TIMEOUT);

wifiManager.autoConnect(AP_SSID, AP_PWD);

}

return (WiFi.status() == WL_CONNECTED);
}

void loop() {
if (EnsureWiFi()) {
// WiFi connection is ready
}
}

what branch are you using ?

The one that the Arduino Studio library manager installs automatically (version appears to be 0.14.0) (see attached screenshot). Should I be using a different, manually installed one?
wifimanagerversion

Also... If scanning for APs is what's causing the issues, I would gladly trade that (i.e. type the ssid/password in a text box, rather than selecting it) in order to have rock solid auto-recovery when possible.

Yeah no support or development is being done on anything but development branch

Got it. I'll clone from https://github.com/tzapu/WiFiManager/tree/development and report back with the results. Thanks!

hopefully this is a known issue and already fixed, but let me know

I am very happy to report that it worked exactly as intended. Also, I noticed that the behavior that enables this library to work as expected is much more sophisticated than it was on the master branch - thank you for improving the code and making it work for my scenario!

yeah it still needs to be documented and examples cleaned up, that is mostly why it is not beta yet

Still same issue with latest versi贸n?

yes there has not been a release since this, use development branch

Which is the link of the working development branch?

Suggestion to the opening post: If the server gets a client request via the AP, disconnect STA for some time (so AP-only mode). Maybe like a watchdog counter (each server-request reset time counter). Maybe even a client connection (before the page-request) may activate this behaviour.

@Bolukan it is worth testing, But the main problem is the ap can be entirely unstable when sta is trying to connect, but it would be nice to find a stable solution to background connecting, ideally users can just use non blocking and make their own logic, this can probably be POC in user code atm using events or WiFi_softap_num_stations,

I am not even sure you can enablesta when in ap atm without it breaking something..

I am aware of the lack of progress. It was quiet timeconsuming (wiping, downloading and analysuing flashes) but bottom-line too difficult for me to create reproducable testcases.

This is not something I will look into until next version

Any progress on this? I have the very same issue reported here #738.

I want to use "AutoConnect" on setup, so It will run once on boot, so lets suppose that It failed to connect to the saved credentials, and that the ConfigPortalTimeout was hit: Now I need It trying to reconnect to saved credentials forever, but It doesn't do that, 'cause when I start up my router/AP, it simply doesn't connect.

When disconnected, I cannot call the "AutoConnect" function again in the loop because it will enter on AP mode, and the really problem of it is that it stucks the code, as it is using an "while(1)" inside library. The code being stuck is a real problem for me, as I am using the NRF24Mesh library, which needs to have its "mesh.update()" function cycled in loop, otherwise it will not keep my mesh network up.

I'm using the latest library version provided by the Arduino IDE's library manager.

I would really appreciate any kind of help.

Hi @aburst42

I have tried your solution and does not work for me. Any idea why?

After losing the wifi connection switch to AP but I turn the wifi on again and does not reconnect.

Cheers

Hi @ftaibi,

Unfortunately, I don't know why the solution wouldn't work for you. I included below the full code of my garage controller. Beyond staying connected to WiFi through power outages and such, it doesn't do much - it just sets a pin to activate the garage door. Maybe give it a go on your Arduino with a copy of WifiManager from the development branch?

~~~~~~~~

include

//needed for library

include

include

include

include

include

define PRINTF_BUF 512 // define the tmp buffer size (change if desired)

// For control
ESP8266WebServer server(80);

//DEBUG
// Change this to true to collects logs
const bool DEBUG_MODE = true;
const int MAX_DEBUG_ENTRIES = 100;
long debugCodes[MAX_DEBUG_ENTRIES][2];
long crtDebugIndex = 0;

void logCode(long code)
{
if (DEBUG_MODE)
{
debugCodes[crtDebugIndex][0] = code;
debugCodes[crtDebugIndex][1] = os_getCurrentTimeSec();

crtDebugIndex++;

if (crtDebugIndex >= MAX_DEBUG_ENTRIES)
{
  crtDebugIndex = 0;
}

}
}

void handleDebugInfo() {
char szTmp[MAX_DEBUG_ENTRIES * 10];

if (DEBUG_MODE)
{
unsigned long days = 0, hours = 0, minutes = 0;

int length = 0;
for (int crtDebugInfoIndex = 0;crtDebugInfoIndex < crtDebugIndex; crtDebugInfoIndex++)
{
  long code = debugCodes[crtDebugInfoIndex][0];
  long val = debugCodes[crtDebugInfoIndex][1];
  days = val / (3600*24);
  val -= days * (3600*24);
  hours = val / 3600;
  val -= hours * 3600;
  minutes = val / 60;
  val -= minutes*60;

  length += sprintf(
              szTmp + length,
              "%02d %02d:%02d:%02d.%02d<br>",
              (int)code,
              (int)days, (int)hours, (int)minutes, (int)val);

}

}
else // !DEBUG_MODE
{
sprintf(szTmp, "Debug Info Not Available - enable DEBUG_MODE in the code");
}

server.send(200, "text/html", szTmp);
}

const uint8_t DOOR_CONTROL_PIN = D7;

// Duration to close the switch on the door opener. This should be long
// enough for the mechanism to start; typically it doesn't have to remain
// activated for the door to complete its motion. It is the same as the
// time you'd hold down the button to start the door moving.
const float DOOR_ACTIVATION_PERIOD = 0.3; // [s]

// For LED status
Ticker ticker;

void tick()
{
//toggle state
int state = digitalRead(LED_BUILTIN); // get the current state of GPIO1 pin
digitalWrite(LED_BUILTIN, !state); // set pin to the opposite state
}

void ledOn() {
ticker.detach();

//keep LED on
digitalWrite(LED_BUILTIN, LOW);
}

void ledBlink(float interval /in seconds/) {
ticker.attach(interval, tick);
}

void configModeCallback (WiFiManager *myWiFiManager) {
ledBlink(0.2);
}

void InitializeHardware() {
// set led pin as output
pinMode(LED_BUILTIN, OUTPUT);
ledBlink(0.5);

// initialize relay pin
pinMode(DOOR_CONTROL_PIN, OUTPUT);
digitalWrite(DOOR_CONTROL_PIN, LOW);
}

void ToggleOn() {
digitalWrite(DOOR_CONTROL_PIN, HIGH);
}

void ToggleOff() {
digitalWrite(DOOR_CONTROL_PIN, LOW);
}

void handleRoot() {
// This returns the 'homepage' with links to each other main page
unsigned long days = 0, hours = 0, minutes = 0;
unsigned long val = os_getCurrentTimeSec();

days = val / (360024);
val -= days * (3600
24);

hours = val / 3600;
val -= hours * 3600;

minutes = val / 60;
val -= minutes*60;

byte mac[6];
WiFi.macAddress(mac);

char szTmp[PRINTF_BUF*2];
sprintf(szTmp, ""
"Dashboard for esp8266 controlled Garage Door Opener.

"
"MAC Address: %02X%02X%02X%02X%02X%02X
"
"Uptime: %02d:%02d:%02d.%02d
"
"Free heap: %u

"
"Web Commands
"
"Toggle
"
"ToggleOn
"
"ToggleOff
"
"Restart
"
"DebugInfo
"
"",
mac[0],mac[1],mac[2],mac[3],mac[4],mac[5],
(int)days, (int)hours, (int)minutes, (int)val,
ESP.getFreeHeap());
server.send(200, "text/html", szTmp);
}

Ticker toggleTicker;
void handleToggle() {
ToggleOn();
toggleTicker.attach(DOOR_ACTIVATION_PERIOD, handleToggleOff);
}

void handleToggleOn() {
ToggleOn();
}

void handleToggleOff() {
toggleTicker.detach();
ToggleOff();
}

void handleRestart() {
server.send(200, "text/plain", "ESP is restarting (can also reset with changed URL)");
server.stop();
delay(2000);

ESP.restart();
}

const unsigned long CONNECT_TIMEOUT = 30; // Wait 30 seconds to connect to the real AP before trying to boot the local AP
const unsigned long AP_TIMEOUT = 180; // Wait 3 minutes in the config portal before trying again the original WiFi creds
const char* AP_SSID = "ARDUINO2"; // Create an access point with this SSID
const char* AP_PWD = "CONFIGURE"; // Protect the access point with this password

// Returns true if connection is successful
// false if connection failed (timeout occurred, or wrong ssid/password)
// Returning false typically means we should loop again to give the original
// ssid/password a chance, after which we try the AP again
bool EnsureWiFi() {
// If we're already connected, do nothing
if (WiFi.status() != WL_CONNECTED) {
logCode(0);

WiFiManager wifiManager;

wifiManager.setAPCallback(configModeCallback);
wifiManager.setDebugOutput(true);

wifiManager.setConnectTimeout(CONNECT_TIMEOUT);

// Exit after the assigned amount of time, regardless of whether we're
// connected or not. This will give the caller a chance to try calling
// EnsureWiFi again, which in turn will try again with the original
// ssid/password (auto-recovery from transient failure), followed by
// the next access point cycle.
wifiManager.setTimeout(AP_TIMEOUT);

// This will block until a WiFi is connected, or the timeout has elapsed
wifiManager.autoConnect(AP_SSID, AP_PWD);

logCode(1);

}

return (WiFi.status() == WL_CONNECTED);
}

void EnsureSerial() {
Serial.begin(115200);
}

bool serverStarted = false;
void EnsureServer() {
if ((WiFi.status() == WL_CONNECTED)
&& (!serverStarted)) {
logCode(2);

server.begin();
serverStarted = true;

logCode(3);

}
}

void EnsureConnections() {
EnsureWiFi();
EnsureSerial();
EnsureServer();
}

void PrepareWebServerHandlers() {
server.on("/", handleRoot);
server.on("/toggle", handleToggle);
server.on("/toggleOn", handleToggleOn);
server.on("/toggleOff", handleToggleOff);
server.on("/restart", handleRestart);
server.on("/debugInfo", handleDebugInfo);
}

void setup() {
InitializeHardware();
PrepareWebServerHandlers();
EnsureConnections();

ArduinoOTA.begin();
Serial.println("setup complete");
}

void loop() {
ArduinoOTA.handle();

ledBlink(0.2);

EnsureConnections();

server.handleClient();

if (WiFi.status() == WL_CONNECTED) {
ledOn();
}

}

unsigned long os_getCurrentTimeSec()
{
static unsigned int wrapCnt = 0;
static unsigned long lastVal = 0;
unsigned long currentVal = millis();

if(currentVal < lastVal)
{
wrapCnt++;
}

unsigned long seconds = currentVal/1000;

//millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter
return (wrapCnt*4294967) + seconds;
}

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Bolukan picture Bolukan  路  12Comments

e-music picture e-music  路  8Comments

tablatronix picture tablatronix  路  8Comments

liebman picture liebman  路  5Comments

bydoctor picture bydoctor  路  5Comments