深入学习 esp8266 wifimanager源码解析(打造专属自己的web配网)(最全的wifimanager介绍))

原文地址:https://my.oschina.net/u/4269090/blog/3329239

1.前言

废话少说,本篇博文的目的就是深入学习 WifiManager 这个github上非常火爆的ESP8266 web配网库,让初学者不再对web配网感到迷茫,并且通过学习第三方库来自定义自己的web配网功能。等你学习整篇博文,你会发现 So easy!!

2.WiFiManager

2.1 WiFiManager源码地址

直接点击 github 下载代码

2.2 WiFiManager是什么

一句话概括:

ESP8266 WiFi连接管理器,主要是提供Web页面配置功能,包括Web配网、自定义参数配置

2.3 安装WiFiManager 库

第一步

这个库是跟ESP8266 Arduino核心库一起使用,所以用户需要配置好Arduino For ESP8266平台,具体请参考 ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境

第二步

可以通过Arduino IDE库管理器安装WiFiManager,搜索“WiFiManager”,然后点击下载(适合新手小白,直接上手使用)

也可以直接通过github下载源码,放到你的Arduino libraries文件夹(深入学习者,可以查看源码然后改造源码,达到自定义配网功能

2.4 WiFiManager工作原理

1、当你的ESP8266上电启动时,它首先进入STA模式,然后尝试去连接之前已经保存过的AP热点(如果你不了解STA和AP的区别,麻烦参考 ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用);

2、如果连接失败(可能是因为没有保存上一次的热点信息),那么该库就会让ESP进入AP模式并且提供AP热点,启动并监听DNS服务以及Web服务(默认ip地址是192.168.4.1)(如果你不了解 DNS服务 和 Web服务的概念,麻烦参考 ESP8266开发之旅 网络篇⑮ DNSServer——真正的域名服务ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用

3、使用任何可以连接WiFi并且可以使用浏览器的设备(包括电脑、手机、平板)去连接ESP生成的AP热点

4、由于 DNS服务 Captive Portal功能的存在,会导致任何的网络请求都重定向到 认证配置页面(也就是我们的Web配置页面)

5、选择任意一个扫描到的AP热点,输入热点密码,点击保存

6、ESP会尝试去连接新AP热点,如果连接成功,就返回到我们的正常业务,连接失败,就会再次重复上面的操作

2.5 如何引入WiFiManager库

非常简单,直接引入:

#include <WiFiManager.h> 

至于怎么样去彻底了解这个库的使用方法,请看博主下面的源码分析。

3.WifiManager源码解析

通过查阅WifiManager的源码,整体代码非常简单,提供出来的方法调用也不多。这里博主就一个个方法详细讲解,并且深入去分析代码,以便后面自定义自己的WifiManager。主要分为几类方法:

  • 配置类方法
  • 连接类方法

博主建议:先了解有什么方法,然后在后面例子讲解中去感受方法的使用

3.1 配置类方法

主要包括:

  • setConfigPortalTimeout
  • setTimeout
  • setConnectTimeout
  • setDebugOutput
  • setMinimumSignalQuality
  • setAPStaticIPConfig
  • setSTAStaticIPConfig
  • setAPCallback
  • setSaveConfigCallback
  • addParameter
  • setBreakAfterConfig
  • setCustomHeadElement
  • setRemoveDuplicateAPs
  • resetSettings
  • getConfigPortalSSID

3.1.1 setConfigPortalTimeout/setTimeout —— 配置认证超时

函数说明

/**
 * 功能描述:配置认证超时
 * @param seconds 秒数为单位
 */
void WiFiManager::setTimeout(unsigned long seconds) {
  setConfigPortalTimeout(seconds);
}

/**
 * 功能描述:配置认证超时
 * @param seconds 秒数为单位
 */
void WiFiManager::setConfigPortalTimeout(unsigned long seconds) {
  _configPortalTimeout = seconds * 1000;
}

注意点

  • 配置界面超时,指的是用户进入配置界面后多少秒之内如果没有处理完毕就退出整个配置过程;
  • 默认情况下,_configPortalTimeout 等于0,也就是不限制时间;

3.1.2 setConnectTimeout —— 设置sta连接超时时间

函数说明

/**
 * 功能描述:设置sta连接超时,超过时间就返回连接状态
 * @param seconds 秒数为单位
 */
void WiFiManager::setConnectTimeout(unsigned long seconds) {
  _connectTimeout = seconds * 1000;
}

注意点

  • 正常情况下,_connectTimeout等于0,也就是直接等待连接状态。

3.1.3 setDebugOutput —— 设置是否打开debug模式

函数说明

/**
 * 功能描述:设置是否打开debug
 * @param debug true or false
 */
void WiFiManager::setDebugOutput(boolean debug) {
  _debug = debug;
}

注意点

  • 建议调试阶段把该功能打开,可以清晰知道整个运行流程。等待调试完毕,关闭调试功能;

3.1.4 setAPStaticIPConfig —— 设置固定AP,包括ip、网关、子网掩码

函数说明

/**
 * 功能描述:设置固定AP,包括ip、网关、子网掩码
 * @param ip  ip地址
 * @param gw  网关地址
 * @param sn  子网掩码
 */
void WiFiManager::setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) {
  _ap_static_ip = ip;
  _ap_static_gw = gw;
  _ap_static_sn = sn;
}

注意点

  • 此方法用于设置ESP在AP模式下固定的AP信息,不设置默认为192.168.4.1

3.1.5 setSTAStaticIPConfig —— 设置固定STA,包括ip、网关、子网掩码

函数说明

/**
 * 功能描述:设置固定STA,包括ip、网关、子网掩码
 * @param ip  ip地址
 * @param gw  网关地址
 * @param sn  子网掩码
 */
void WiFiManager::setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) {
  _sta_static_ip = ip;
  _sta_static_gw = gw;
  _sta_static_sn = sn;
}

注意点

  • 此方法用于设置ESP在STA模式下固定的STA信息,不设置默认由DHCP分配;
  •  

3.1.6 setMinimumSignalQuality —— 设置最小的信号质量

函数说明

/**
 * 功能描述:设置能够接受的最低信号强度
 * @param quality  信号强度,默认为8
 */
void WiFiManager::setMinimumSignalQuality(int quality) {
  _minimumQuality = quality;
}

注意点

  • 此方法用来过滤信号强度低于quality的AP热点,用于scan扫描时过滤;

3.1.7 setBreakAfterConfig —— 设置WEB配置失败后是否退出配置

函数说明

/**
 * 功能描述:设置WEB配置失败后是否退出配置
 * @param shouldBreak 是否退出配置,默认是false
 */
void WiFiManager::setBreakAfterConfig(boolean shouldBreak) {
  _shouldBreakAfterConfig = shouldBreak;
}

3.1.8 setAPCallback —— 设置开启AP模式配置时的通知回调

函数说明

/**
 * 功能描述:设置开启AP模式配置时的通知回调
 * @param func 回调函数
 */
void WiFiManager::setAPCallback( void (*func)(WiFiManager* myWiFiManager) ) {
  _apcallback = func;
}

注意点

  • 此方法主要是告知我们已经进入了配置模式了;

3.1.9 setSaveConfigCallback —— 设置保存Web配置后的回调

函数说明

//start up save config callback
/**
 * 功能描述:设置保存Web配置后的回调
 * @param func 回调函数
 */
void WiFiManager::setSaveConfigCallback( void (*func)(void) ) {
  _savecallback = func;
}

注意点

  • 此方法主要是告知我们已经点击保存配置信息了;
  •  

3.1.10 addParameter —— 配置页面添加自定义参数

函数说明

/**
 * 功能描述:配置页面添加自定义参数
 * @param WiFiManagerParameter 自定义参数
 */
bool WiFiManager::addParameter(WiFiManagerParameter *p) {
  if(_paramsCount + 1 > _max_params)
  {
    // rezise the params array
    _max_params += WIFI_MANAGER_MAX_PARAMS;
    DEBUG_WM(F("Increasing _max_params to:"));
    DEBUG_WM(_max_params);
    WiFiManagerParameter** new_params = (WiFiManagerParameter**)realloc(_params, _max_params * sizeof(WiFiManagerParameter*));
    if (new_params != NULL) {
      _params = new_params;
    } else {
      DEBUG_WM(F("ERROR: failed to realloc params, size not increased!"));
      return false;
    }
  }

  _params[_paramsCount] = p;
  _paramsCount++;
  DEBUG_WM(F("Adding parameter"));
  DEBUG_WM(p->getID());
  return true;
}

注意点

  • 普通情况下我们一般只配置SSID和Password,如果你还需要添加额外的参数(比如MQTT IP和port等等),那么这个方法就比较有用;

WiFiManagerParameter:

class WiFiManagerParameter {
  public:
    /** 
        Create custom parameters that can be added to the WiFiManager setup web page
        @id is used for HTTP queries and must not contain spaces nor other special characters
    */
    WiFiManagerParameter(const char *custom);
    WiFiManagerParameter(const char *id, const char *placeholder, const char *defaultValue, int length);
    WiFiManagerParameter(const char *id, const char *placeholder, const char *defaultValue, int length, const char *custom);
    ~WiFiManagerParameter();

    // 这里比较需要注意的是 id值,这个会在html submit的时候返回给webserver,一定不能包含空格或者其他特殊字符
    const char *getID();
    const char *getValue();
    const char *getPlaceholder();
    int         getValueLength();
    const char *getCustomHTML();
  private:
    const char *_id;
    const char *_placeholder;
    char       *_value;
    int         _length;
    const char *_customHTML;

    void init(const char *id, const char *placeholder, const char *defaultValue, int length, const char *custom);

    friend class WiFiManager;
};

3.1.11 setCustomHeadElement——设置自定义页面的头部元素

函数说明

/**
 * 功能描述:添加一个自定义的element到页面头部 比如加个头像
 * @param element element样式的字符串
 */
void WiFiManager::setCustomHeadElement(const char* element) {
  _customHeadElement = element;
}

注意点

  • 如果你觉得官方提供的界面样式看起来不爽,那么你可以试试这个方法,试图去改变点样式;

3.1.12 setRemoveDuplicateAPs——设置是否过滤重复的AP热点

函数说明

//if this is true, remove duplicated Access Points - defaut true
/**
 * 功能描述:设置是否过滤重复的AP热点,默认过滤
 * @param removeDuplicates true or false,default true
 */
void WiFiManager::setRemoveDuplicateAPs(boolean removeDuplicates) {
  _removeDuplicateAPs = removeDuplicates;
}

3.1.13 resetSettings——重置配置

函数说明

/**
 * 功能描述:重置配置
 * @param seconds 秒数为单位
 */
void WiFiManager::resetSettings() {
  DEBUG_WM(F("settings invalidated"));
  DEBUG_WM(F("THIS MAY CAUSE AP NOT TO START UP PROPERLY. YOU NEED TO COMMENT IT OUT AFTER ERASING THE DATA."));
  // 断开wifi连接,设置当前配置SSID和pwd为null 设置为true,那么就会关闭Station模式
  WiFi.disconnect(true);
  //delay(200);
}

3.1.14 getConfigPortalSSID——获取WEB配置的SSID

函数说明

/**
 * 功能描述:获取AP配置的SSID
 */
String WiFiManager::getConfigPortalSSID() {
  return _apName;
}

3.2 连接类方法

所谓连接类方法,也就是跟wifi连接过程中相关的方法。此类方法包括:

  • autoConnect
  • startConfigPortal

3.2.1 autoConnect —— 自动连接到上一次保存的AP热点,如果连接失败,进入AP配网模式

函数说明

/**
 * 功能说明:自动连接到上一次保存的AP热点,如果连接失败,进入AP配网模式
 */
boolean WiFiManager::autoConnect() {
  // 默认的AP ssid
  String ssid = "ESP" + String(ESP.getChipId());
  return autoConnect(ssid.c_str(), NULL);
}

/**
 * 功能说明:自动连接到上一次保存的AP热点,如果连接失败,进入AP配网模式
 * @param apName ap模式下的名字
 * @param apPassword ap模式下的密码
 */
boolean WiFiManager::autoConnect(char const *apName, char const *apPassword) {
  DEBUG_WM(F(""));
  DEBUG_WM(F("AutoConnect"));

  // read eeprom for ssid and pass  这里可以从eeprom获取ssid和password
  //String ssid = getSSID();
  //String pass = getPassword();

  // attempt to connect; should it fail, fall back to AP
  WiFi.mode(WIFI_STA);

  if (connectWifi("", "") == WL_CONNECTED)   {
    DEBUG_WM(F("IP Address:"));
    DEBUG_WM(WiFi.localIP());
    //connected
    return true;
  }
  // 如果连接失败 直接进入配网管理
  return startConfigPortal(apName, apPassword);
}

注意点

  • 这两个方法都会自动连接到上一次保存的AP热点,连接失败,就会进入web配网页面(也就是 startConfigPortal 这个方法)。用户可以自定义AP模式下的SSID和Password(也就是调用方法2)。

connectWifi方法代码:

/**
 * 功能描述:连接Ap热点
 * @param ssid 热点名称
 * @param pass 热点密码
 * @return 连接状态
 */
int WiFiManager::connectWifi(String ssid, String pass) {
  DEBUG_WM(F("Connecting as wifi client..."));

  // check if we've got static_ip settings, if we do, use those.
  // 判断我们是否配置了sta(ip地址、网关地址、子网掩码),有配置就设置配置内容
  if (_sta_static_ip) {
    DEBUG_WM(F("Custom STA IP/GW/Subnet"));
    WiFi.config(_sta_static_ip, _sta_static_gw, _sta_static_sn);
    DEBUG_WM(WiFi.localIP());
  }
  //fix for auto connect racing issue
  // 如果连接成功了 直接返回
  if (WiFi.status() == WL_CONNECTED && (WiFi.SSID() == ssid)) {
    DEBUG_WM(F("Already connected. Bailing out."));
    return WL_CONNECTED;
  }
  //check if we have ssid and pass and force those, if not, try with last saved values
  // 判断有没有传入自定义的 ssid和 pass,没有就用上次存储的
  if (ssid != "") {
    // 连接wifi
    WiFi.begin(ssid.c_str(), pass.c_str());
  } else {
    if (WiFi.SSID() != "") {
      DEBUG_WM(F("Using last saved values, should be faster"));
      //trying to fix connection in progress hanging
      ETS_UART_INTR_DISABLE();
      wifi_station_disconnect();
      ETS_UART_INTR_ENABLE();
      // 连接wifi
      WiFi.begin();
    } else {
      DEBUG_WM(F("No saved credentials"));
    }
  }

  // 等待连接结果
  int connRes = waitForConnectResult();
  DEBUG_WM ("Connection result: ");
  DEBUG_WM ( connRes );
  //not connected, WPS enabled, no pass - first attempt
  #ifdef NO_EXTRA_4K_HEAP
  if (_tryWPS && connRes != WL_CONNECTED && pass == "") {
    startWPS();
    //should be connected at the end of WPS
    connRes = waitForConnectResult();
  }
  #endif
  return connRes;
}

/**
 * 功能描述:等待连接结果
 * 如果没有设置连接超时 直接返回状态
 * 如果设置了连接超时 在规定时间内等待连接状态
 */
uint8_t WiFiManager::waitForConnectResult() {
  if (_connectTimeout == 0) {
    return WiFi.waitForConnectResult();
  } else {
    DEBUG_WM (F("Waiting for connection result with time out"));
    unsigned long start = millis();
    boolean keepConnecting = true;
    uint8_t status;
    while (keepConnecting) {
      status = WiFi.status();
      // 判断连接是否超时
      if (millis() > start + _connectTimeout) {
        keepConnecting = false;
        DEBUG_WM (F("Connection timed out"));
      }
      if (status == WL_CONNECTED) {
        keepConnecting = false;
      }
      delay(100);
    }
    return status;
  }
}

注意点

  • DEBUG_WM debug功能的实现由 setDebugOutput 控制
  • _sta_static_ip 的值 由 setSTAStaticIPConfig 设置
  • waitForConnectResult 里面 的 _connectTimeoutsetConnectTimeout 设置

3.2.2 startConfigPortal —— 启动web配置

函数说明

函数说明

/**
 * 功能说明:配置认证,默认ssid为esp+chipid
 */
boolean WiFiManager::startConfigPortal() {
  String ssid = "ESP" + String(ESP.getChipId());
  return startConfigPortal(ssid.c_str(), NULL);
}

/**
 * 功能说明:配置认证,切换到AP模式 然后配置webserver和dnsserver,响应web请求
 * @param apName ap名字
 * @param apPassword ap密码
 */
boolean  WiFiManager::startConfigPortal(char const *apName, char const *apPassword) {

  // 如果没有连接上网络 切换到AP模式
  if(!WiFi.isConnected()){
    WiFi.persistent(false);
    // disconnect sta, start ap
    WiFi.disconnect(); //  this alone is not enough to stop the autoconnecter
    WiFi.mode(WIFI_AP);
    WiFi.persistent(true);
  } 
  else {
    //setup AP 切换到AP-STA模式
    WiFi.mode(WIFI_AP_STA);
    DEBUG_WM(F("SET AP STA"));
  }


  _apName = apName;
  _apPassword = apPassword;

  //notify we entered AP mode
  // 通知我们进入AP模式,可以执行回调方法
  if ( _apcallback != NULL) {
    _apcallback(this);
  }

  connect = false;
  // 配置信息 这是非常关键的一个方法,主要是配置webserver、DNSserver
  setupConfigPortal();

  while(1){

    // check if timeout 判断是否配置超时
    if(configPortalHasTimeout()) break;

    //DNS 处理dns请求
    dnsServer->processNextRequest();
    //HTTP 处理web请求
    server->handleClient();


    if (connect) {
      connect = false;
      delay(2000);
      DEBUG_WM(F("Connecting to new AP"));

      // using user-provided  _ssid, _pass in place of system-stored ssid and pass
      // 使用用户提供的wifi账号密码来设置
      if (connectWifi(_ssid, _pass) != WL_CONNECTED) {
        DEBUG_WM(F("Failed to connect."));
      } else {
        //connected
        WiFi.mode(WIFI_STA);
        //notify that configuration has changed and any optional parameters should be saved
        // 通知外面wifi连接成功,可以做一些额外操作
        if ( _savecallback != NULL) {
          //todo: check if any custom parameters actually exist, and check if they really changed maybe
          _savecallback();
        }
        break;
      }
      // 判断配置后是否断开
      if (_shouldBreakAfterConfig) {
        //flag set to exit after config after trying to connect
        //notify that configuration has changed and any optional parameters should be saved
        if ( _savecallback != NULL) {
          //todo: check if any custom parameters actually exist, and check if they really changed maybe
          _savecallback();
        }
        break;
      }
    }
    yield();
  }

  server.reset();
  dnsServer.reset();

  return  WiFi.status() == WL_CONNECTED;
}

注意点

  • configPortalHasTimeout 用来判断是否在认证界面配置超时
  • _apcallback 通过 setAPCallback 设置
  • _savecallback 通过 setSaveConfigCallback 设置,主要用于配置完毕后保存一些信息;
  • _shouldBreakAfterConfig 通过 setBreakAfterConfig 设置,主要用来判断配置失败后是否保存信息退出;

关键方法 setupConfigPortal(一定要看博主在源码中的注释)

/**
 * 功能描述:配置web服务以及dns重定向功能
 */
void WiFiManager::setupConfigPortal() {
  // 配置dns服务(这里主要是用到重定向功能)
  dnsServer.reset(new DNSServer());
  // 配置web服务 主要用来显示web页面 实现AP配网之类的功能
  server.reset(new ESP8266WebServer(80));

  DEBUG_WM(F(""));
  _configPortalStart = millis();

  DEBUG_WM(F("Configuring access point... "));
  DEBUG_WM(_apName);
  if (_apPassword != NULL) {
    if (strlen(_apPassword) < 8 || strlen(_apPassword) > 63) {
      // fail passphrase to short or long!
      DEBUG_WM(F("Invalid AccessPoint password. Ignoring"));
      _apPassword = NULL;
    }
    DEBUG_WM(_apPassword);
  }

  //optional soft ip config
  // 配置自定义固定AP信息
  if (_ap_static_ip) {
    DEBUG_WM(F("Custom AP IP/GW/Subnet"));
    WiFi.softAPConfig(_ap_static_ip, _ap_static_gw, _ap_static_sn);
  }

  // 配置AP热点
  if (_apPassword != NULL) {
    WiFi.softAP(_apName, _apPassword);//password option
  } else {
    WiFi.softAP(_apName);
  }

  delay(500); // Without delay I've seen the IP address blank
  DEBUG_WM(F("AP IP address: "));
  DEBUG_WM(WiFi.softAPIP());

  /* Setup the DNS server redirecting all the domains to the apIP */
  // 配置DNS 重定向功能 全部请求都重定向到AP
  dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
  dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());

  /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
  // 配置Web服务器的功能 这里就是实现AP配网功能
  server->on(String(F("/")).c_str(), std::bind(&WiFiManager::handleRoot, this));
  server->on(String(F("/wifi")).c_str(), std::bind(&WiFiManager::handleWifi, this, true));
  server->on(String(F("/0wifi")).c_str(), std::bind(&WiFiManager::handleWifi, this, false));
  server->on(String(F("/wifisave")).c_str(), std::bind(&WiFiManager::handleWifiSave, this));
  server->on(String(F("/i")).c_str(), std::bind(&WiFiManager::handleInfo, this));
  server->on(String(F("/r")).c_str(), std::bind(&WiFiManager::handleReset, this));
  //server->on("/generate_204", std::bind(&WiFiManager::handle204, this));  //Android/Chrome OS captive portal check.
  server->on(String(F("/fwlink")).c_str(), std::bind(&WiFiManager::handleRoot, this));  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
  server->onNotFound (std::bind(&WiFiManager::handleNotFound, this));
  server->begin(); // Web server start
  DEBUG_WM(F("HTTP server started"));
}

注意点

  • _ap_static_ip 通过 setAPStaticIPConfig 设置,设置AP模式下的固定IP信息
  • handleRoot 处理域的web请求,比如 192.168.4.1
  • handleWifi 处理web配置页面的请求
  • handleWifiSave 处理wifi配置信息保存请求
  • handleInfo 处理wifi信息的展示
  • handleReset 处理wifi配置重置的请求

接下来,重点讲解handleXXX方法的具体内容以及展示页面(后面打造专属自己的web配网也是从这里修改)。

3.2.2.1 handleRoot —— 根目录页面

函数说明

/** Handle root or redirect to captive portal */
/**
 * 功能描述:响应root请求,也就是ip请求
 */
void WiFiManager::handleRoot() {
  DEBUG_WM(F("Handle root"));
  if (captivePortal()) { // If caprive portal redirect instead of displaying the page.
    return;
  }
  // 这里就会显示一个通用页面了
  String page = FPSTR(HTTP_HEADER);
  page.replace("{v}", "Options");
  page += FPSTR(HTTP_SCRIPT);
  page += FPSTR(HTTP_STYLE);
  page += _customHeadElement;
  page += FPSTR(HTTP_HEADER_END);
  page += String(F("<h1>"));
  page += _apName;
  page += String(F("</h1>"));
  page += String(F("<h3>WiFiManager</h3>"));
  page += FPSTR(HTTP_PORTAL_OPTIONS);
  page += FPSTR(HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);
}

注意点

  • 此方法是整个配置页面的入口页面,会包含后面一些列操作的入口
  • 这里拼接了一个web页面返回,包括自定义的 _customHeadElement

Root 页面的HTML+CSS+JS代码(这里是我们后面打造专属自己样式的关键)

const char HTTP_HEADER[] PROGMEM          = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\"/><title>{v}</title>";
const char HTTP_STYLE[] PROGMEM           = "<style>.c{text-align: center;} div,input{padding:5px;font-size:1em;} input{width:95%;} body{text-align: center;font-family:verdana;} button{border:0;border-radius:0.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;} .q{float: right;width: 64px;text-align: right;} .l{background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAALVBMVEX///8EBwfBwsLw8PAzNjaCg4NTVVUjJiZDRUUUFxdiZGSho6OSk5Pg4eFydHTCjaf3AAAAZElEQVQ4je2NSw7AIAhEBamKn97/uMXEGBvozkWb9C2Zx4xzWykBhFAeYp9gkLyZE0zIMno9n4g19hmdY39scwqVkOXaxph0ZCXQcqxSpgQpONa59wkRDOL93eAXvimwlbPbwwVAegLS1HGfZAAAAABJRU5ErkJggg==\") no-repeat left center;background-size: 1em;}</style>";
const char HTTP_SCRIPT[] PROGMEM          = "<script>function c(l){document.getElementById('s').value=l.innerText||l.textContent;document.getElementById('p').focus();}</script>";
const char HTTP_HEADER_END[] PROGMEM        = "</head><body><div style='text-align:left;display:inline-block;min-width:260px;'>";
const char HTTP_PORTAL_OPTIONS[] PROGMEM  = "<form action=\"/wifi\" method=\"get\"><button>Configure WiFi</button></form><br/><form action=\"/0wifi\" method=\"get\"><button>Configure WiFi (No Scan)</button></form><br/><form action=\"/i\" method=\"get\"><button>Info</button></form><br/><form action=\"/r\" method=\"post\"><button>Reset</button></form>";
const char HTTP_ITEM[] PROGMEM            = "<div><a href='#p' onclick='c(this)'>{v}</a>&nbsp;<span class='q {i}'>{r}%</span></div>";
const char HTTP_FORM_START[] PROGMEM      = "<form method='get' action='wifisave'><input id='s' name='s' length=32 placeholder='SSID'><br/><input id='p' name='p' length=64 type='password' placeholder='password'><br/>";
const char HTTP_FORM_PARAM[] PROGMEM      = "<br/><input id='{i}' name='{n}' maxlength={l} placeholder='{p}' value='{v}' {c}>";
const char HTTP_FORM_END[] PROGMEM        = "<br/><button type='submit'>save</button></form>";
const char HTTP_SCAN_LINK[] PROGMEM       = "<br/><div class=\"c\"><a href=\"/wifi\">Scan</a></div>";
const char HTTP_SAVED[] PROGMEM           = "<div>Credentials Saved<br />Trying to connect ESP to network.<br />If it fails reconnect to AP to try again</div>";
const char HTTP_END[] PROGMEM             = "</div></body></html>";

Root页面效果

这里包含了多个功能入口

  • Configure WiFi、Configure WiFi(No Scan)—— handleWifi
  • Info —— handleInfo
  • Reset —— handleReset

3.2.2.2 handleWifi —— 配置Wifi信息

用户点击 Configure WiFi、Configure WiFi(No Scan) 就会响应此方法。

函数说明

/** Wifi config page handler */
/**
 * 功能描述:wifi配置页面
 * @param scan true or false,是否开启扫描
 */
void WiFiManager::handleWifi(boolean scan) {

  String page = FPSTR(HTTP_HEADER);
  page.replace("{v}", "Config ESP");
  page += FPSTR(HTTP_SCRIPT);
  page += FPSTR(HTTP_STYLE);
  page += _customHeadElement;
  page += FPSTR(HTTP_HEADER_END);

  // 判断是否扫描
  if (scan) {
    // 获取扫描结果
    int n = WiFi.scanNetworks();
    DEBUG_WM(F("Scan done"));
    if (n == 0) {
      DEBUG_WM(F("No networks found"));
      page += F("No networks found. Refresh to scan again.");
    } else {

      //sort networks
      int indices[n];
      for (int i = 0; i < n; i++) {
        indices[i] = i;
      }

      // RSSI SORT

      // old sort 排序信号强度,高的放在前面
      for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
          if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
            std::swap(indices[i], indices[j]);
          }
        }
      }

      /*std::sort(indices, indices + n, [](const int & a, const int & b) -> bool
        {
        return WiFi.RSSI(a) > WiFi.RSSI(b);
        });*/

      // remove duplicates ( must be RSSI sorted )
      // 是否去掉重复的AP
      if (_removeDuplicateAPs) {
        String cssid;
        for (int i = 0; i < n; i++) {
          if (indices[i] == -1) continue;
          cssid = WiFi.SSID(indices[i]);
          for (int j = i + 1; j < n; j++) {
            if (cssid == WiFi.SSID(indices[j])) {
              DEBUG_WM("DUP AP: " + WiFi.SSID(indices[j]));
              indices[j] = -1; // set dup aps to index -1
            }
          }
        }
      }

      //display networks in page
      // 拼接每一个需要显示的ap热点信息
      for (int i = 0; i < n; i++) {
        if (indices[i] == -1) continue; // skip dups
        DEBUG_WM(WiFi.SSID(indices[i]));
        DEBUG_WM(WiFi.RSSI(indices[i]));
        int quality = getRSSIasQuality(WiFi.RSSI(indices[i]));

        // 判断信号强度是否大于我们设置的最小强度
        if (_minimumQuality == -1 || _minimumQuality < quality) {
          String item = FPSTR(HTTP_ITEM);
          String rssiQ;
          rssiQ += quality;
          item.replace("{v}", WiFi.SSID(indices[i]));
          item.replace("{r}", rssiQ);
          if (WiFi.encryptionType(indices[i]) != ENC_TYPE_NONE) {
            item.replace("{i}", "l");
          } else {
            item.replace("{i}", "");
          }
          //DEBUG_WM(item);
          page += item;
          delay(0);
        } else {
          DEBUG_WM(F("Skipping due to quality"));
        }

      }
      page += "<br/>";
    }
  }

  page += FPSTR(HTTP_FORM_START);
  char parLength[5];
  // add the extra parameters to the form
  // 添加我们自定义的参数
  for (int i = 0; i < _paramsCount; i++) {
    if (_params[i] == NULL) {
      break;
    }

    String pitem = FPSTR(HTTP_FORM_PARAM);
    if (_params[i]->getID() != NULL) {
      pitem.replace("{i}", _params[i]->getID());
      pitem.replace("{n}", _params[i]->getID());
      pitem.replace("{p}", _params[i]->getPlaceholder());
      snprintf(parLength, 5, "%d", _params[i]->getValueLength());
      pitem.replace("{l}", parLength);
      pitem.replace("{v}", _params[i]->getValue());
      pitem.replace("{c}", _params[i]->getCustomHTML());
    } else {
      pitem = _params[i]->getCustomHTML();
    }

    page += pitem;
  }
  if (_params[0] != NULL) {
    page += "<br/>";
  }

  if (_sta_static_ip) {

    String item = FPSTR(HTTP_FORM_PARAM);
    item.replace("{i}", "ip");
    item.replace("{n}", "ip");
    item.replace("{p}", "Static IP");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_ip.toString());

    page += item;

    item = FPSTR(HTTP_FORM_PARAM);
    item.replace("{i}", "gw");
    item.replace("{n}", "gw");
    item.replace("{p}", "Static Gateway");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_gw.toString());

    page += item;

    item = FPSTR(HTTP_FORM_PARAM);
    item.replace("{i}", "sn");
    item.replace("{n}", "sn");
    item.replace("{p}", "Subnet");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_sn.toString());

    page += item;

    page += "<br/>";
  }

  page += FPSTR(HTTP_FORM_END);
  page += FPSTR(HTTP_SCAN_LINK);

  page += FPSTR(HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);


  DEBUG_WM(F("Sent config page"));
}

注意点

  • _minimumQualitysetMinimumSignalQuality 设置

上图是启用了scan功能的效果。

上图没有启用scan功能,通常用于隐藏SSID的配置。 关于scan功能,麻烦读者参考 ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用

3.2.2.3 handleWifiSave —— 保存Wifi信息

用户在wifi配置页面点击 save 操作就会响应此方法。

函数说明

/** Handle the WLAN save form and redirect to WLAN config page again */
/**
 * 功能描述:保存wifi设置
 */
void WiFiManager::handleWifiSave() {
  DEBUG_WM(F("WiFi save"));

  //SAVE/connect here 保存用户设置的ssid和psw
  _ssid = server->arg("s").c_str();
  _pass = server->arg("p").c_str();

  //parameters
  // 保存用户设置的自定义参数
  for (int i = 0; i < _paramsCount; i++) {
    if (_params[i] == NULL) {
      break;
    }
    //read parameter
    String value = server->arg(_params[i]->getID()).c_str();
    //store it in array
    value.toCharArray(_params[i]->_value, _params[i]->_length + 1);
    DEBUG_WM(F("Parameter"));
    DEBUG_WM(_params[i]->getID());
    DEBUG_WM(value);
  }

  if (server->arg("ip") != "") {
    DEBUG_WM(F("static ip"));
    DEBUG_WM(server->arg("ip"));
    //_sta_static_ip.fromString(server->arg("ip"));
    String ip = server->arg("ip");
    optionalIPFromString(&_sta_static_ip, ip.c_str());
  }
  if (server->arg("gw") != "") {
    DEBUG_WM(F("static gateway"));
    DEBUG_WM(server->arg("gw"));
    String gw = server->arg("gw");
    optionalIPFromString(&_sta_static_gw, gw.c_str());
  }
  if (server->arg("sn") != "") {
    DEBUG_WM(F("static netmask"));
    DEBUG_WM(server->arg("sn"));
    String sn = server->arg("sn");
    optionalIPFromString(&_sta_static_sn, sn.c_str());
  }

  // 拼接一个 wifi 保存的页面通知 确保响应了
  String page = FPSTR(HTTP_HEADER);
  page.replace("{v}", "Credentials Saved");
  page += FPSTR(HTTP_SCRIPT);
  page += FPSTR(HTTP_STYLE);
  page += _customHeadElement;
  page += FPSTR(HTTP_HEADER_END);
  page += FPSTR(HTTP_SAVED);
  page += FPSTR(HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

  DEBUG_WM(F("Sent wifi save page"));

  connect = true; //signal ready to connect/reset
}

3.2.2.4 handleInfo —— 获取ESP的信息

用户在wifi配置页面点击 Info 操作就会响应此方法。

函数说明

/** Handle the info page */
/**
 * 功能描述:展示详情信息
 */
void WiFiManager::handleInfo() {
  DEBUG_WM(F("Info"));

  String page = FPSTR(HTTP_HEADER);
  page.replace("{v}", "Info");
  page += FPSTR(HTTP_SCRIPT);
  page += FPSTR(HTTP_STYLE);
  page += _customHeadElement;
  page += FPSTR(HTTP_HEADER_END);
  page += F("<dl>");
  // 芯片id
  page += F("<dt>Chip ID</dt><dd>");
  page += ESP.getChipId();
  page += F("</dd>");
  // flash 芯片id
  page += F("<dt>Flash Chip ID</dt><dd>");
  page += ESP.getFlashChipId();
  page += F("</dd>");
  // flash大小
  page += F("<dt>IDE Flash Size</dt><dd>");
  page += ESP.getFlashChipSize();
  page += F(" bytes</dd>");
  page += F("<dt>Real Flash Size</dt><dd>");
  page += ESP.getFlashChipRealSize();
  page += F(" bytes</dd>");
  page += F("<dt>Soft AP IP</dt><dd>");
  // AP ip地址
  page += WiFi.softAPIP().toString();
  page += F("</dd>");
  page += F("<dt>Soft AP MAC</dt><dd>");
  // AP mac地址
  page += WiFi.softAPmacAddress();
  page += F("</dd>");
  page += F("<dt>Station MAC</dt><dd>");
  page += WiFi.macAddress();
  page += F("</dd>");
  page += F("</dl>");
  page += FPSTR(HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

  DEBUG_WM(F("Sent info page"));
}

 

3.2.2.5 handleReset —— 重置Wifi模块信息

用户在wifi配置页面点击 Reset 操作就会响应此方法。

函数说明

/** Handle the reset page */
void WiFiManager::handleReset() {
  DEBUG_WM(F("Reset"));

  String page = FPSTR(HTTP_HEADER);
  page.replace("{v}", "Info");
  page += FPSTR(HTTP_SCRIPT);
  page += FPSTR(HTTP_STYLE);
  page += _customHeadElement;
  page += FPSTR(HTTP_HEADER_END);
  page += F("Module will reset in a few seconds.");
  page += FPSTR(HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

  DEBUG_WM(F("Sent reset page"));
  delay(5000);
  // 重置信息
  ESP.reset();
  delay(2000);
}

 

4.官方实例操作

使用WifiManager三部曲:

  • 引入头文件
#include <WiFiManager.h>
  • 配置具体参数
  • 启动WifiManager服务

请读者烧录以下代码: 例子源码

/**
 *  功能:测试WifiManager功能demo
 *  作者:单片机菜鸟
 *  时间:2019-12-13
 *  描述:
 *      1.启动wifimanager AP配置页面,通过手机连接esp8266生成的AP热点来进入设置页面
 *      2.请在手机浏览器上输入 192.168.4.25
 *      2.测试官方方法的使用
*/

#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino

//needed for library
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson
//for LED status
#include <Ticker.h>

void initSystem();
void initWifiManager();
void configModeCallback(WiFiManager *myWiFiManager);
void saveConfigCallback();
void tick();

//flag for saving data
bool shouldSaveConfig = false;
//for LED status
Ticker ticker;

//define your default values here, if there are different values in config.json, they are overwritten.
char mqtt_server[40];
char mqtt_port[6] = "8080";
char api_key[34] = "Your ApiKey";

void setup() {
  // put your setup code here, to run once:
  initSystem();
  initWifiManager();
  //if you get here you have connected to the WiFi
  Serial.println("connected...so easy :)");
  ticker.detach();
  //keep LED on
  digitalWrite(BUILTIN_LED, LOW);
}

void loop() {
  // put your main code here, to run repeatedly:

}

/**
 * 功能描述:初始化esp8266
 */
void initSystem(){
  Serial.begin(115200);
  Serial.println();
  //set led pin as output
  pinMode(BUILTIN_LED, OUTPUT);
  // start ticker with 0.5 because we start in AP mode and try to connect
  ticker.attach(0.6, tick);
}

/**
 * 功能描述:初始化wifimanager
 */
void initWifiManager(){
  /***  步骤一:创建 wifimanager对象 **/
  WiFiManager wifiManager;
  /*************************************/
  /*** 步骤二:进行一系列配置,参考配置类方法 **/
  // 重置保存的修改 目标是为了每次进来都是去掉配置页面
  wifiManager.resetSettings();
  // 配置连接超时
  wifiManager.setConnectTimeout(60);
  // 打印调试内容
  wifiManager.setDebugOutput(true);
  // 设置最小信号强度
  wifiManager.setMinimumSignalQuality(30);
  // 设置固定AP信息
  IPAddress _ip = IPAddress(192, 168, 4, 25);
  IPAddress _gw = IPAddress(192, 168, 4, 1);
  IPAddress _sn = IPAddress(255, 255, 255, 0);
  wifiManager.setAPStaticIPConfig(_ip, _gw, _sn);
  // 设置进入AP模式的回调
  wifiManager.setAPCallback(configModeCallback);
  // 设置点击保存的回调
  wifiManager.setSaveConfigCallback(saveConfigCallback);
  // 设置 如果配置错误的ssid或者密码 退出配置模式
  wifiManager.setBreakAfterConfig(true);
  // 设置过滤重复的AP 默认可以不用调用 这里只是示范
  wifiManager.setRemoveDuplicateAPs(true);
  // 添加额外的参数 博哥这里只是示范 比如加入 mqtt 服务器地址 port 端口号  apikey 后面可以结合onenet使用
  WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40);
  WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6);
  WiFiManagerParameter custom_apikey("apikey", "onenet apikey", api_key, 32);
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);
  wifiManager.addParameter(&custom_apikey);

  /*************************************/
  /*** 步骤三:尝试连接网络,失败去到配置页面 **/
  // ssid 命名为danpianjicainiao pwd是123456
  if (!wifiManager.autoConnect("danpianjicainiao","123456")) {
      Serial.println("failed to connect and hit timeout");
      //reset and try again, or maybe put it to deep sleep
      ESP.reset();
      delay(1000);
  }
  /*************************************/
  // 读取配置页面配置好的信息
  strcpy(mqtt_server, custom_mqtt_server.getValue());
  strcpy(mqtt_port, custom_mqtt_port.getValue());
  strcpy(api_key, custom_apikey.getValue());

  // 保存自定义信息
  if (shouldSaveConfig) {
      Serial.println("saving config");
      DynamicJsonBuffer jsonBuffer;
      JsonObject& json = jsonBuffer.createObject();
      json["mqtt_server"] = mqtt_server;
      json["mqtt_port"] = mqtt_port;
      json["api_key"] = api_key;
      json.printTo(Serial);
      //end save
  }

  Serial.println("local ip");
  Serial.println(WiFi.localIP());
}

/**
 * 功能描述:配置进入AP模式通知回调
 */
void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("Entered config mode");
  Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  Serial.println(myWiFiManager->getConfigPortalSSID());
  //entered config mode, make led toggle faster
  ticker.attach(0.2, tick);
}

/**
 * 功能描述:设置点击保存的回调
 */
void saveConfigCallback () {
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

/**
 * 功能描述:设置LED灯闪烁,提示用户进入配置模式
 */
void tick(){
  //toggle state
  int state = digitalRead(BUILTIN_LED);  // get the current state of GPIO1 pin
  digitalWrite(BUILTIN_LED, !state);     // set pin to the opposite state
}

这里重点讲解了各个方法的使用,并且通过led灯闪烁来表示工作模式,请读者自行下载烧录体会。没有意外情况的下会有以下控制页面(直接看博主截图说明):

输入密码后,就可以连接上新wifi。 由于代码中打开了串口调试功能,所以可以看到以下调试信息:

美中不足

  • 对于习惯于中文的初学者来说,肯定是希望可以支持中文;
  • 对于爱好潮流的人来说,肯定希望打造自己的web配网页面;

下面就是博主基于wifiManger去打造专属自己的web配网,暂且命名为 CustomWifiManager。此库,博主已经上传到QQ技术交流群。

5.打造专属自己的web配网

5.1 CustomWifiManager —— 站在巨人肩膀上

博主基于WifiManger改造了几个方面,适合简单的样式修改。

5.1.1 setHeadImgBase64 —— 支持自定义ICON头像

函数说明

void WiFiManager::setHeadImgBase64(String imgBase64) {
  _imgBase64= imgBase64;
}

5.1.2 setButtonBackground—— 自定义按钮背景颜色

函数说明

void WiFiManager::setButtonBackground(const char* backgroundColor){
  _backgroundColor= backgroundColor;
}

5.1.3 setButtonTextColor—— 自定义按钮字体颜色

函数说明

void WiFiManager::setButtonTextColor(const char* textColor) {
  _textColor= textColor;
}

5.1.4 setPageTitle—— 自定义页面标题

函数说明

void WiFiManager::setPageTitle(const char* pageTitle){
  _pageTitle = pageTitle;
}

5.2 案例

例子源码

/**
 *  功能:测试WifiManager功能demo
 *  作者:单片机菜鸟
 *  时间:2019-12-13
 *  描述:
 *      1.启动wifimanager AP配置页面,通过手机连接esp8266生成的AP热点来进入设置页面
 *      2.请在手机浏览器上输入 192.168.4.25
 *      3.分别支持大叔黄 萌弟蓝 少女粉
*/

#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino

//needed for library
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <CustomWiFiManager.h>         //https://github.com/tzapu/WiFiManager
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson
//for LED status
#include <Ticker.h>

// 自定义我的icon 请随便使用一个图片转base64工具转换
const char yellowWifi[] PROGMEM = "data:image/png;base64,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yqKo0OXf6/P5CDc0cPZML3vuu5cPBibZu9OgqkwSSxbeDRacAqahj6E/9ynoGoJ/PQp1Yb2hkGr2upsFotBoZdsMwyCZlOzd+wX+7MtfgVSCcH39RVY+1NR6lxwd4Yfff4FvfuspTnaM8MDtJp9uds97lF70SCyrGz4TnvuZPhJLZvRW2W8yi1MCDU7BzoTAcRSRSC3bt2+fAi17UjTzoGTW84BSEpm3Yriuy+DAIEfb3uH4yQ+J1MB9m0y2rXNJO7kYLgoAPXrenA7CSa/Kc7wXhmKzg3WlLoDI82QhSoEn2kULJdPB04EkMzltyZplQFMj/O4qkz+4TrJisWIiqSM/H6/mXRdwJZQHwGfAyCT0DXsFTqFFrtwPr5yEF97U9YFs/aCQFZvbMy1bRoslYfctsOHaXM1QKc3QJbWSqjKF7eg6wnyPxue9FzAN3WnCOyhdvWT2CXBzVK8Wz/0UQgFd8XULsNotdt43o9+UrQ8399wKO27SZ/9TI+j5kHZ01UiIC6sLXNBu0BC6R1flqjVT5k2VPbfC0jo4+LIesYogUyyZrwlytYXxBNSGYd8fwqbVMJ4s8MwFBj713KUujkqvONo5CN/9Cfy0Q18v8+u5CrkfS8wMOrsQOG6OxreshD//NFxZT9Hl7GLtkgMAOTEEOPpLOHxcl8cnkrmRMvJGLFsadySgoDoEv3MN3L4GWpu8stcFzOuSAwC5s/hQQI9215Aup717BvpGdJYZ8wCpLNOANUXguiu0llzdoL9j0tsHzafM9WsFQD4QoIUzYGn9sB0tbCmvxlDm121+SwOS9kTvcgaetcv+G6FsAGlb0zi7nlsmVHq9S6nreGnbS7XF5Q/8VwZA1oSYfi6vFDgeO7ICeLl/EFVSAOYEpZSde/ax/6HkAgCldqDUtgBAqR0otS0AUGoHSm0LAJTagVLbAgCldqDU9n+vbbJY13ln1QAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMS0wOVQwMjo0Njo0MCswODowMELuAxIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDEtMDlUMDI6NDY6NDArMDg6MDAzs7uuAAAAQ3RFWHRzb2Z0d2FyZQAvdXNyL2xvY2FsL2ltYWdlbWFnaWNrL3NoYXJlL2RvYy9JbWFnZU1hZ2ljay03Ly9pbmRleC5odG1svbV5CgAAABh0RVh0VGh1bWI6OkRvY3VtZW50OjpQYWdlcwAxp/+7LwAAABh0RVh0VGh1bWI6OkltYWdlOjpIZWlnaHQANTEyj41TgQAAABd0RVh0VGh1bWI6OkltYWdlOjpXaWR0aAA1MTIcfAPcAAAAGXRFWHRUaHVtYjo6TWltZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADE1NDY5NzMyMDBYBo4+AAAAEnRFWHRUaHVtYjo6U2l6ZQAzMjE5MUJVXU35AAAAYnRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vaG9tZS93d3dyb290L25ld3NpdGUvd3d3LmVhc3lpY29uLm5ldC9jZG4taW1nLmVhc3lpY29uLmNuL2ZpbGVzLzExMi8xMTIzMjIxLnBuZ7vHiF8AAAAASUVORK5CYII=";
const char blueWifi[] PROGMEM = "data:image/png;base64,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ZXguaHRtbL21eQoAAABjdEVYdHN2Zzpjb21tZW50ACBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIM5IkAsAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6SGVpZ2h0ADQxMo5PObYAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgANTI03jL1KgAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNTQ2OTc3NzgzgH8AMAAAABJ0RVh0VGh1bWI6OlNpemUAMjA4NDVC9X3U4QAAAGJ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL2hvbWUvd3d3cm9vdC9uZXdzaXRlL3d3dy5lYXN5aWNvbi5uZXQvY2RuLWltZy5lYXN5aWNvbi5jbi9maWxlcy8xMjAvMTIwNTg1OS5wbmctUab6AAAAAElFTkSuQmCC";
const char pinkWifi[] PROGMEM = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH4wEJAAoyLlQcvAAAGW9JREFUeNrtmnm0XXd13z/f/TvnvtGyBsuSbGuwLFke8EAMeEI2xsaURUIAm3RhIAwNSSghhAUL0oShKclaUFpTp1AvhgaCTWkgCbNT8GzAAza2bJBlydYsS9Zs6Q16797z++3+ce4599wnkeCErnat8nvrvDPcM/z2vPd3/+BX4//vof/bEwB46p4ttCcj+VDAYwLAzHADHFZevuw5v/OBnzzaJbAk8UUXnPv/JgOevGsLqJyoux+XUjpLMCGzLYPZ0Hg7TjNdHCEPLVa+5NSf+571d24iKpGTAeBeMjGlRPLEyPQQB0cOc+5lq/5xBmz98a6Sb6UgcHMAlr5o0S+d+Kfu2orjZCEjpvhS4EPu/gJJU8BGnFuBT2Uh212kDiDG501y/vPOBODxuzfw5LJ1rNr0PAKBIRtg0qeOEzopJT/ZPV3hsAPjmyHaM49duYlTHj6Biy4472gGbH1gDxmBSEQYyTstgKDQLjnqJE84jlxMzJ/irJWn/uLUzhhrfvoEoweGgQTo5bh/2sUWoW8CeEpXgy52fIukP89b+bc7nU6UQyCQQiqljDFdTI0GZWfiXIOzGnEqMNthqPyA35nEGw2ekYvTX9qbtwB+9uRWdn7iSU5/6zlYsFlyfxnub5UUgHXArcn9Kff0DK4x5LiXD4/NO8LzVi1/zgx44vZNBDMknYX4H8i/mPC/Ch4OS3BkemqwlbfOlXgXrisQf4P8z0gcBmGZ8pR8cYrpte7+G8A5OHMAvEmdO15O9k8Y5eMcIZ35khX9DNh8325MwvGVJt2AcwUw2DUCBFPufsDdNyN+AtxUxLgmD1mR3AkOheDUS3pmsnbDJgBm7RwmtlL3Y8JxcIhFpBOLoSyEm5E/HbP0PivUHolDbDmwjblz5xFkkDGgpFcYuh7xk+T+F4JTQG/D/fkOS3HkFeXyn8fz7ZiuAjZIxorVi8s5rVm/ieMPjiAxQOKvgX8980lVPO29ezfwPcQXgIc67c6440jUhFaOTRLWMhEZEFLFAEOpE4vf9JT+IMlfT+LpUqMmeP45Z/Ho2icY2TcEAZh0GNLVhj6HM+QwjPsIpeM8Bq3es26pOnShdwI3JhIrL1ta/rzxhzvpzvwCSd8TzOt7QfcD9ZXGBx3G3P3HuN8QU7zHTMNAS2geYpYnvwBYAQwDiyVZ+ZwjR0jLZfrjNB2/PHDaCIenDnH2Gaf1kfLkvVuRIEwYPujXyvUFh9FStaspes+buVNLol/R6Qrt1cDUisuWAJC5wNsFyu1fuTOvSaNwjmJw/aMAjsP9SuBCoXXuPgsYchiVaxgYqAKxu9NTJC8P3Z8icbdyUeye5uwLT+PHj/yUkX2DpOCoA1NjU4Qs0BrKcfNvWcduw/3V3iBNUikOqY/4ShPpaiJiudAcYFd1T5ZShMCgp/TSYylSjxm9yTd/cHcEo8AL5VSk1faiHrEzhAGIm6beuWfH0I0LWHFhKZGhPS2SJSwP5p20SFLbZIcnnp2cbrXydsiz7+H+KhxrClhdodR+hoopPVNEDCGGmjRmKSaQjsNZ0Jx+RYOXTrBJU213lVT77ukx5RjsFI1vjAN3ZNfPRbG8+4HvrqluHvUj6U8RlwFjRVFMWm43dVKxIbbTlUKm5lubUu8eN/fVhjRLMB+xaeP9T3PaRSeTgcAZBmZRT7FHPO59mlDrgdNwQP2mckwGuHfvq33KXvCtAMWJ0+VkEwQF2kX7dxS0GNM7caZwLlPi092wHIEHXXTc/RTgJCDrqXtDwRrEd7cBpLmVpm58YAdZEQuAMWAfaHEl5qOdq/cdepegpiZUx9XWp/FHv3CH4wcAfu2sM8orCcYmx7I8yy929y9nCmvOffuZrPns49fiFI7faMG+lmXZLqSYijTf3a8MFt4r6QwcvPSuOEyBHxRqd/1CC5gr+BiB9vDTw7dPLJ4go3TMR8AP1wblZUirYysVUbVq9DGlZkCDSKn0fupdqB1U1xDuXjRx4sT2456pn4lDiQXtE4uxMLYjyUfNAw9/Ye35ecze5Mb7lu5c9NUtC58muXct3cdbRf75NMIaJX1NaJmkrYjPC92L2GGyqe5HBxGny/RBSZ9vnzx9TSvmD1tXNR0nNpSHruvoP++zp558pa5Vlpld0+aOMgN3LwuU5Dt3Du7hPVffUP9sbhzwgxSp+ErsFK+e9valtP3dndj5wXSY/sZTJ2+jyCJ7Wvs456pVxMHE1Gib1qHwUDt2buwUnWeKGN9cTHb+HLgD2ABsA7ZJ2iD0HaS3u5On6O9oe8xMQRyZ6Ew53JOSk5KTvNxi47gScGX75ZaaBOGeyntTwlMipUTsbqm7eUpdjXGcxCdv/cOaAUdGp2mrwwXfueYhM/tLc/2xuV0r9I3WVN4eZYQXXHUOV11+KQC/dvnZmIupoTbg35E0jnvSgIFBkrPi8iWsuHwJGhQEI7qvNbP7g9nqVsjmm1yEHNqdzqOdoig6RUGn06Hd6dApOhRFQVEUxKKgiOVxESMxxgZhXhOYvLul/i12nylirN6ZxSLi5jzy/XUAvOTSC8kscP9vfBVz+6EnPuvuY47viEQ2a9tMP8J5V5xJCIHc8vFgwUMeXhey8LzZJ8xBwIa7twCw/IWnEI4YA5Ynk+2V2SxJo9mukYPMK44DeAo4CMxvenMANe2/z/y9Nu+e/6CXfnazMjWyM6+1yV/hgZtHNXKwXUzz8HfWlibXl9T5JBA8+RDACQPzjmLAQ4+uhb2lBQHPJKWvBgtvPrTv2XuHi6GvT+ZH6nsHLKND6k609JbZlS86n0dufQpgv9BBd+aLngevfb2qzErUpeCMvKD5DMyIAn1DSLrSk//d4UOH1lmwe4KFXY5LQMKzFNNlkq6QaZ6kVYq677FLN3HfQ2u4+AXn128a3j2AZYbLVwodSuaPKGhcSR9rtzo/ychqtemMJlxJIYURkAvFDCClBHDAZJuA05tk9DKsvvlTRYuub+upRe0s6NOCKp9oaEwLdEXIsisEb3e8qL4ipBDCYJ3gGG9OA/7d829fuZcR+Omd67FW4OxLVmA/CESLQ1kRfhfjWwO0JlLLt1jbWjiLuk6wdrK4BkBLkI4Abfvx2icJIVRbzEIgBCOEgHW3+tgMqdrU2KvP4ZdRweq9mZV7qT6WVJ6Xz+dCQ5R1xBAw2K0gKoZcFgr7z8hPzg8GQhGwI2L97ZvBOL6V8g+b7AyZ3aUhG8lj9mqTHTGzp2Rlxrz9mQNYMMzC8Wa2wMyeVrAD2ZyDs5gOU4Cf485FfZI6Suil6eCVrTvu3SLX1c0djs4JZiZB9bsaF8p8vd+IemaHCb1RUefGPH3BsEeEiIpLNa1rPWi9pHUW9WdM+SIXLZneHztxfz7SKu3/mYxExMVpglMQN+UeJrLpYgrwDOftZRkr+hLtyit5qdNdV1ZaqzdMRXTPy2ysYuTMTPDY9XtP2s0Cpt73+Hae0PVC0wAmG5TsYXd/U4yxnWXZMsMWufuW2QPHb97vB0mdyJ51h6GA1nmz6aw5fG2Xz3dHd/T43dsBlgN3Akv6pFRrgzfSXe8LBrWkmtdmEt98V5VDNIn2ukDu40WVcKmZcDU3E2Y2YbLPOP4xk+3tzrYs0CQGB4fBKNHioHNwbhEcdOOlwL5MCDNd7e6L64Kilnw3afXSNzezv5oIn0nI0QT3399fK3hfxOlnQh8DKjPp/hkGCRJpBPEemS5CfEzSHa2sNVH6HAOJjc9u08o5yy40578gnSLYKnEEQOt/sGNE0t8DV0OF25UTrdV9Zo1c0XUMtdYxiO9XhGbkaBRUHH1vXcc3Na1b88t6e6ucsWnCZPfL7LNm4RGTpmQ6Dnib0BskLew65v2IV8j1YBZCWIxzRtMKrRZJ1+51TFoKoFOafldFvU+xexJvakgjZM7UgkrqlSN1lRD8DF/QNbmetjouSYPACHAlsNrd9zu0SQxIWihTHblMmofp95XpsSwST5FrdhOxbaJ+fcADjIMeBb4Lvg73gwnhnhqWM1P1U88Euscz06OGa+0jsnpOCJdjWG0CJQhTo7AmaVmK6WUuf7FSWiyzRS4hC1gNjhhmCXdDrquJrMgMnSZpuFZ5NRjgtejd3e9A+hikH6fkh2tMpgmS9lDKHtENLThWeD3WMBnuTpZlWRYyhSz0a0ED5qqc4b6d++4cnxr/0sJFC5fkef5Gs/BuSfOq3MVMvX2ZhywQXJQF2UmSsn79oqkGEbg+xfhxxP6iiGRZjnti5UtO5pcx1t++EciQEkXRGTDZizC93j2dUcTCilT0HGGXaFN/6T1nwZx9c5n7bZm+efILn/7o0w8ufsBCuNGk5UdFDyiTL3Gqtt67Z00yP6+J2NJnp3xSpg/iPrnghPls2b6ds698bi2xJ24vmyQx9iAHmTjrqrJD8+3P3cqpS5bSyvOlKaX/ALwKMbuHQVQYX4+ISqJ1pCivFyZ7WNIHYkp3tfLWlZJuNrOFtf1bD6uQs1bb7t877mJkJrDZHY968l8HduCzCNl4X2/A3ZGM5KkPOWqag+N4dMYnxoOZzSpiHBsZHipCCCxfvZgn79xCTJGU0klm9hW5LusVld3ssCc16KXPFQOipN2SCsRck41KthV4a5bld0p8UNK/lxSsAdhgQpGdWcJbPRvuSZ7Sy3/S3XeELBDjmEBnCea7iMBa4ABl8nQq4EjrcN9LWVKfSRlQNmHsHh4Z/khK6RUDcJfMPpSSj1cWNzQ0ZEcmJ9/r7pe5e/Lk33J8g2TWY0Av5CXhErOldJ2kb5nZR1QWN5e5+38DXyrp+hiLV4aQ/XfHrzM4M3X9nHDkBqiVmUQdUWrbF+C7he4lOJ4cI7kne0vE/9CdCP67uN/snt7j7u909wj+R+7+GXf/A+ADjiec33P3fwCuBV+ZYKFi/DQl/kDRKSg6naWSrunaeVvSf82y7I7tW3dp4+an/DM3fIaTFszh2dnGiuMW89G/+AgTkxNLJb1C0v3Ak5LhiR9KPial42V2nuDlU5MTXxgZHn7ApTNrXKLyH4GhzMwKRN6Xu5axdXtyf6biSggZyf1WSe+UMyQ4U7KBmOKZ7p7jniN/eTD7YrtTLMd9wN3HHd+44vIl+9bfsfn9iN9y9+958h0AWZaRPCHp+Z58Uf11kdqdDvMXzvUTF12YX3Txi7Ke7Rtj42Mpy7KBrkb8tqT1wNMh2Nskndi9VzJ7zfDg4JfM7BakN2CW98wKSBSZF/6sh7JbohofBYkpQwWCxRfOZ/sDexCsJ7EHfGmCiz3Gk4Aldf/AfVWMcVEXrwfYLWnrtkd2kWXZRnf/ont61q1k66zRWQQzHC5DtHrM73KiBG3eJek1iIiqJow8xjhobie6fNDMfl1iP3CWzPJQOTk4DWm2hbALqZCUl2GQ0o8E9mVk3Cvpmtr0VSfDfZ2GVGaFe4EtOEuFLwDOcPcTnQogZS7uy919QTearHV8XzwUB8H/k7u/FNjkzlUutktGLO1yoC876B62Qgt3Pwvx4r70V2qmwI/H6H+yfPXCsZ0P7n+ZpMsthEErPf4o0qCZCdHNAaowCrT4QeYtvm8dfhPIvCmCmtu9WSnFSUJ4DLjcXXPc42qkEbkiyMBnJffzgNGuGG/BmU4pDYEPuJO5+2Cvbi7BVHf3Zqpbg0ml/3lY0lxJhVIdBl3JLkryJZJcUrHt/j0IFVXa7sFxDIGnEMrs0btaXq0miNqYWbRNiEnBrLqm1zHaW+4oBIDvgn5H8oXJ9F6cVvL0GeB5wKUy/am7zwYmhNbWEvUuDll3XMphZkeV11XO2A5t8pR/FvE5M2t0o8wcvkRVvlfevTT9rnY0w+UMxKr830ZstNTyJ4Fd7tQ9gFSVrKkX15devKDC+zckT3tjSvLkrZQc4IfJ033dZ+Z58uDJ9yRP20qI3HtQeaxg9K6+Vb91ewzNyjJMBxyXu2cppYAIkoJMuUmZdTNCsxKuqzd1YbdgyEwWhAUjBBF6zEkmTWfTs4pdgwfy+xVZJa8gr26Fp35Yowue7nF8qztLSt+XJt19U0rpgLu/y0vvj7s/6e57q8ZJLdm6DBa33XxPaQINs6vKXYA8z0me3iXpNSaLNcYok6QzrYk7hlBqgRmhOjdzwLsuoNFTFogDBDZlgztCOw3GbytynUPezIZmojoJZ93N902ccd3Fj+O+ulvhHQB2ppLYg+6+sNv9uUXSkaIo+ohvVnyzZ81mbHysDIVdsr1bBgPlEj1nFfDiCmOqOrsz29+hkeXVAK7ZPpzDqhx7DWc6ErfYnLAuS+VSqMeRdss5pYd2+wjuLYe6syCJlb/1AlKMt7int7p7C9gJ7E+xyN3Z7e4L3X3K3R+rfEkvqNGssNl/cD9AZrJF1fsrXluwsiOVYuzW8N1c3mqk2UppewghTk1Pk2dZkhkGBAeDLSSbTMlPlcjV7RyrpOlrvitOZylzioG4YeBI9lcq9OFeTs8ySUuAn26+bzenXryAUy85iQ13bsXdfwT6fcq1P1sdJrt0foQScR0XWtPz6XRA14N/1fFDOAd6PzEneVpRgxyQdZsm8yMxIJ1VR6FGNddTU19YxPhqS2lS6GoLdWrfSe5ffuDBv06rL33LKyVl3SQQxFqMR2SgrQ/tLTN2YxVtbsc5udf45P2dzvQnBgaG6HQ6rFx9Mhvu2lrTVXllB4pOp5pQH1jR6V5voDdUjfN2exrgVZK+ghgWVlV+hWTeJThTOXqS72aEXW3wYNYp1T7kIZhCyLBgd6SUrhkZGZoL3C5jWQWMYPxRa9xuGF9UkLUXJeyA44HNeSfci/vrGjjdvw0hvyXGYu3K1Sez5f7dLLtoAf/S8aOvPVROHk40Cx8oie+13UVXWjPq+Cq09Tx+wILJzFqhZEC3mWPrzez9I3NHD8UjnY+aaVm9hE/aj3F3Z5YTOtUKrouczdfvJg34lWrzt558dhUGwf+XpLe02+3d3aSFc69e+c8m/o6bfkiet0gpzgNukNkbat9c1fzdLI+a4NCH6JhZ3c2y/v14COH2PB/44M4d23+25NRl7whBnzDTSBUJkL7uQ3oTYmJkYRk5WLtpO9m4gTEYJuxLFLzOU6x6/R5j/G5RxPcNDg6ub09PMzk5SdEpCFngktc+/zkx4LYv3UOK0UKW/RvJXo88VG65XlhZqXd13PXqoXJ81XkIhCxTsDAh0/dl9mBRFA+Pjo7EELJ3m9m/C0FzyneBBTYyoDdQ8MDAcU5xfNbzJzvu3st4a5pOK56VT4WvKPq55YKGGsxcB3yyKOI/jI+N7ZkzZ057enoaTwnciTFWaW2XceVCiViDoT1oOQs509MTAzElUa2wcS+zwq4fyPKAyciyDLNyn+d533nIMvIsI8uzdOjgs50TFiwY9pTOD1l4r2SvNNOAhS56hMYU9Mb8aX2rfY4zPCvQabTiefiJTdASoz9KxLMHLmA83kj0F9YrP8qFDp0U07aU0npgm5dZTl/DIyXvW11WQgLU583FVXWro4H00LD3qqF6rH1oZn5mHrJsNMuy00OWrQohzAndJq8FI5iSTB/PBsJHoqeOD0B+Qqh9cz1++sR2OlZw/NgwxcT0uZ74VEppdSnZ3moQby55qZbEeO96taCqigjNFltda1Yfb8Bdza7yTG9vM9S/YkLl/EIwQpaVx1nV7TZCsKSgv/TMP6yoMbkYOS2jnX7O+oWf3LsechgZH6RIcam7f8hTui6lNNQkvmZCgwGN6q6UdmpKu39BI42YXku2KeWGw6sJD6FBtDXb+jOcYcUwPYv4uHI+lTo+7tEJKzJGyqIOoLHctDsuuGQVQ50BJnaMg7NVpnfGlN6WUnospVgBmP0Ln7pMqPxA73qvwPFGodVcSFUvUjtqO/Yao3Jralpj0XSlSQDyhwi8ceSE7D/ijMuhtSLvI/6YGlCNex/5GaN7BiCI1I5Ej6fEGK9z99/DWd50kCUR/cQeuzfYa2zUvdemzVcLKCrph9BMefs0ITuG9EMIHvKwOYTwWeQ3tch2xrysM2yJMWhHyfufbNLwyPeeIAwG2mNtFg6cwDPt/ctjUbw2xfTbntJp7j7c5wf6GFD/q/GlGpOvnGCN+1s/8Q1Vr8vd0LD7iugyTO6zYGvd+ZssD98fu+3wxnmvnEd7TiQ/bBy/fPDn0vdPMgDg27fdzYLJuSQ5mQIL5sxn264dJ7r7eQbvcLjAUzrB3YebEaH5lb6l6zOu90Fc1mWAGs6vItTq5TuTIYQDFsJTWRb+NmTZD1qDA5vHD4yNDR43yMDIALGI2NKcWa3wj9L2CzGgGl+/5TYWTM4jqQSb3J2BPB+MKS4sirgMOC/F+MqU0ukOJ+A+7LjwGZ9UF5uvuz3MqPREKKVeWAjtYHbIzHaHLGzMQnabhbDezLaaae9QPjTmGWR5RjaUkzqROauGfmGanhMDmuO+v3uYTrvNwOAQuFMUkWDGobHDw4IT8zxfbGbL3X2Wu18GnAjMlTSrigaNbK+QtEtmUdIek+42syizZ4LZPjPbg7HXYXy4NdRWGd7KztCAoIDOwsRpSxY+Zzr+2Qxojq994xbmT8yFIIp2m5ScPMvqsNcpOi3JQjAdJ2moqvTq/D5YMgsHJSUzRU/ebsb68rg0E8/L1vg5l6/4F876l8iAY42b/uffc9WhS1gzvI6W51SLr3o9vZ4GVD38KgnyzDn70dO479LHeNlLLvk/NcVfjV8N4H8DvpM4yZ/1GykAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTktMDEtMDlUMDA6MTA6NTArMDg6MDCSUWrCAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE5LTAxLTA5VDAwOjEwOjUwKzA4OjAw4wzSfgAAAEN0RVh0c29mdHdhcmUAL3Vzci9sb2NhbC9pbWFnZW1hZ2ljay9zaGFyZS9kb2MvSW1hZ2VNYWdpY2stNy8vaW5kZXguaHRtbL21eQoAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6SGVpZ2h0ADI1NunDRBkAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMjU2ejIURAAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNTQ2OTYzODUwFYbWHQAAABJ0RVh0VGh1bWI6OlNpemUAMzY2MDFCMtvoDwAAAGB0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL2hvbWUvd3d3cm9vdC9uZXdzaXRlL3d3dy5lYXN5aWNvbi5uZXQvY2RuLWltZy5lYXN5aWNvbi5jbi9maWxlcy81Mi81MjU5NjgucG5n+YEv+wAAAABJRU5ErkJggg==";

void initSystem();
void initWifiManager();
void configModeCallback(WiFiManager *myWiFiManager);
void saveConfigCallback();
void tick();

//flag for saving data
bool shouldSaveConfig = false;
//for LED status
Ticker ticker;

//define your default values here, if there are different values in config.json, they are overwritten.
char mqtt_server[40];
char mqtt_port[6] = "8080";
char api_key[34] = "Your ApiKey";

void setup() {
  // put your setup code here, to run once:
  initSystem();
  initWifiManager();
  //if you get here you have connected to the WiFi
  Serial.println("connected...so easy :)");
  ticker.detach();
  //keep LED on
  digitalWrite(BUILTIN_LED, LOW);
}

void loop() {
  // put your main code here, to run repeatedly:

}

/**
 * 功能描述:初始化esp8266
 */
void initSystem(){
  Serial.begin(115200);
  Serial.println();
  //set led pin as output
  pinMode(BUILTIN_LED, OUTPUT);
  // start ticker with 0.5 because we start in AP mode and try to connect
  ticker.attach(0.6, tick);
}

/**
 * 功能描述:初始化wifimanager
 */
void initWifiManager(){
  /***  步骤一:创建 wifimanager对象 **/
  WiFiManager wifiManager;
  /*************************************/
  /*** 步骤二:进行一系列配置,参考配置类方法 **/
  // 重置保存的修改 目标是为了每次进来都是去掉配置页面
  wifiManager.resetSettings();
  // 配置连接超时
//  wifiManager.setConnectTimeout(240);
  // 打印调试内容
  wifiManager.setDebugOutput(true);
  // 设置个人图标
  // 大叔黄
  //wifiManager.setHeadImgBase64(FPSTR(yellowWifi));
  //wifiManager.setButtonBackground("#E08E00");
  // 萌弟蓝
  wifiManager.setHeadImgBase64(FPSTR(blueWifi));
  wifiManager.setButtonBackground("#2394BC");
  // 少女粉
  //wifiManager.setHeadImgBase64(FPSTR(pinkWifi));
  //wifiManager.setButtonBackground("#D5BADB");
  // 设置最小信号强度
  wifiManager.setMinimumSignalQuality(40);
  // 设置固定AP信息
  IPAddress _ip = IPAddress(192, 168, 4, 25);
  IPAddress _gw = IPAddress(192, 168, 4, 1);
  IPAddress _sn = IPAddress(255, 255, 255, 0);
  wifiManager.setAPStaticIPConfig(_ip, _gw, _sn);
  // 设置进入AP模式的回调
  wifiManager.setAPCallback(configModeCallback);
  // 设置点击保存的回调
  wifiManager.setSaveConfigCallback(saveConfigCallback);
  // 设置 如果配置错误的ssid或者密码 退出配置模式
  wifiManager.setBreakAfterConfig(true);
  // 设置过滤重复的AP 默认可以不用调用 这里只是示范
  wifiManager.setRemoveDuplicateAPs(true);
  // 添加额外的参数 博哥这里只是示范 比如加入 mqtt 服务器地址 port 端口号  apikey 后面可以结合onenet使用
  WiFiManagerParameter custom_mqtt_server("server", "mqtt server", mqtt_server, 40);
  WiFiManagerParameter custom_mqtt_port("port", "mqtt port", mqtt_port, 6);
  WiFiManagerParameter custom_apikey("apikey", "onenet apikey", api_key, 32);
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);
  wifiManager.addParameter(&custom_apikey);

  /*************************************/
  /*** 步骤三:尝试连接网络,失败去到配置页面 **/
  // ssid 命名为danpianjicainiao pwd是123456
  if (!wifiManager.autoConnect("danpianjicainiao","123456")) {
      Serial.println("failed to connect and hit timeout");
      //reset and try again, or maybe put it to deep sleep
      ESP.reset();
      delay(1000);
  }
  /*************************************/
  // 读取配置页面配置好的信息
  strcpy(mqtt_server, custom_mqtt_server.getValue());
  strcpy(mqtt_port, custom_mqtt_port.getValue());
  strcpy(api_key, custom_apikey.getValue());

  // 保存自定义信息
  if (shouldSaveConfig) {
      Serial.println("saving config");
      DynamicJsonBuffer jsonBuffer;
      JsonObject& json = jsonBuffer.createObject();
      json["mqtt_server"] = mqtt_server;
      json["mqtt_port"] = mqtt_port;
      json["api_key"] = api_key;
      json.printTo(Serial);
      //end save
  }

  Serial.println("local ip");
  Serial.println(WiFi.localIP());
}

/**
 * 功能描述:配置进入AP模式通知回调
 */
void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("Entered config mode");
  Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  Serial.println(myWiFiManager->getConfigPortalSSID());
  //entered config mode, make led toggle faster
  ticker.attach(0.2, tick);
}

/**
 * 功能描述:设置点击保存的回调
 */
void saveConfigCallback () {
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

/**
 * 功能描述:设置LED灯闪烁,提示用户进入配置模式
 */
void tick(){
  //toggle state
  int state = digitalRead(BUILTIN_LED);  // get the current state of GPIO1 pin
  digitalWrite(BUILTIN_LED, !state);     // set pin to the opposite state
}

请读者安装好自定义库之后烧录以上代码进行测试,不出意外的话,就是以下页面效果:

大叔黄(博主命名请勿喷)

萌弟蓝(博主命名请勿喷)

少女粉(博主命名请勿喷

这里,博主仅仅是列举了三个样式,读者可以自行打造专属自己的web配网。但是博主需要提醒读者几个小点:

  • icon不宜太复杂太大,建议使用小图标,比如博主从 这里 下载。下载完图标之后需要转成base64,可以通过 图片在线转换 来获取。
  • 最好是图标的颜色和button的颜色一样,读者可以通过 传图识色 来获取颜色值。

6.总结

本篇主要是讲解wifimanger的源码解析,并且尝试去打造专属自己的web配网。 如果你觉得本篇对你有帮助,麻烦点赞关注,谢谢支持。本篇内容涵盖了多方面知识,希望读者可以打好基础,多去阅读博主关于esp8266系列的文章,我敢保证肯定收益匪浅。

猜你喜欢

转载自blog.csdn.net/kim5659/article/details/113619656