/* WIFI MODULE Copyright (C) 2016-2019 by Xose PĂ©rez */ #include "wifi.h" #include "wifi_config.h" #include "telnet.h" #include "ws.h" bool _wifi_wps_running = false; bool _wifi_smartconfig_running = false; bool _wifi_smartconfig_initial = false; WiFiApMode _wifi_ap_mode = WiFiApMode::Fallback; #if WIFI_GRATUITOUS_ARP_SUPPORT unsigned long _wifi_gratuitous_arp_interval = 0; unsigned long _wifi_gratuitous_arp_last = 0; #endif // ----------------------------------------------------------------------------- // PRIVATE // ----------------------------------------------------------------------------- namespace settings { namespace internal { template <> WiFiApMode convert(const String& value) { switch (value.toInt()) { case 0: return WiFiApMode::Disabled; case 1: return WiFiApMode::Enabled; default: case 2: return WiFiApMode::Fallback; } } template<> WiFiSleepType_t convert(const String& value) { switch (value.toInt()) { case 2: return WIFI_MODEM_SLEEP; case 1: return WIFI_LIGHT_SLEEP; case 0: default: return WIFI_NONE_SLEEP; } } } // namespace internal } // namespace settings String _wifiSettingsSoftApSsid() { return getSetting("wifiApSsid", strlen(WIFI_AP_SSID) ? F(WIFI_AP_SSID) : getSetting("hostname", getIdentifier())); } String _wifiSettingsSoftApPass() { return getSetting("wifiApPass", strlen(WIFI_AP_PASS) ? F(WIFI_AP_PASS) : getAdminPass()); } void _wifiUpdateSoftAP() { if (!WiFi.softAPgetStationNum()) { // Note: we know that c_str() will be copied, no need to persist it ourselves jw.setSoftAP( _wifiSettingsSoftApSsid().c_str(), #if USE_PASSWORD _wifiSettingsSoftApPass().c_str() #else nullptr #endif ); } } void _wifiCheckAP() { if ( (WiFiApMode::Fallback == _wifi_ap_mode) && ((WiFi.getMode() & WIFI_AP) > 0) && jw.connected() && (WiFi.softAPgetStationNum() == 0) ) { jw.enableAP(false); } } void _wifiConfigure() { jw.setHostname(getSetting("hostname", getIdentifier()).c_str()); _wifiUpdateSoftAP(); jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); wifiReconnectCheck(); _wifi_ap_mode = getSetting("wifiApMode", WIFI_AP_MODE); jw.enableAPFallback(_wifi_ap_mode != WiFiApMode::Disabled); jw.cleanNetworks(); // If system is flagged unstable we do not init wifi networks #if SYSTEM_CHECK_ENABLED if (!systemCheck()) return; #endif unsigned char index = 0; for (index = 0; index < WIFI_MAX_NETWORKS; index++) { const auto ssid = getSetting({"ssid", index}, _wifiSSID(index)); const auto pass = getSetting({"pass", index}, _wifiPass(index)); if (!ssid.length()) { auto current = index; do { delSetting({"ssid", index}); delSetting({"pass", index}); delSetting({"ip", index}); delSetting({"gw", index}); delSetting({"mask", index}); delSetting({"dns", index}); } while (++index < WIFI_MAX_NETWORKS); index = current; break; } bool result = false; if (ssid.length() && pass.length()) { result = jw.addNetwork( ssid.c_str(), pass.c_str(), getSetting({"ip", index}, _wifiIP(index)).c_str(), getSetting({"gw", index}, _wifiGateway(index)).c_str(), getSetting({"mask", index}, _wifiNetmask(index)).c_str(), getSetting({"dns", index}, _wifiDNS(index)).c_str() ); } else if (ssid.length()) { result = jw.addNetwork(ssid.c_str(), pass.c_str()); } if (!result) break; } #if JUSTWIFI_ENABLE_SMARTCONFIG if (index == 0) _wifi_smartconfig_initial = true; #endif jw.enableScan(getSetting("wifiScan", 1 == WIFI_SCAN_NETWORKS)); const auto sleep_mode = getSetting("wifiSleep", WIFI_SLEEP_MODE); WiFi.setSleepMode(sleep_mode); #if WIFI_GRATUITOUS_ARP_SUPPORT _wifi_gratuitous_arp_last = millis(); _wifi_gratuitous_arp_interval = getSetting("wifiGarpIntvl", secureRandom( WIFI_GRATUITOUS_ARP_INTERVAL_MIN, WIFI_GRATUITOUS_ARP_INTERVAL_MAX )); #endif const auto tx_power = getSetting("wifiTxPwr", WIFI_OUTPUT_POWER_DBM); WiFi.setOutputPower(tx_power); } struct wifi_scan_info_t { String ssid_scan; int32_t rssi_scan; uint8_t sec_scan; uint8_t* BSSID_scan; int32_t chan_scan; bool hidden_scan; char buffer[128]; }; template void _wifiScan(WiFiScanCallback callback) { DEBUG_MSG_P(PSTR("[WIFI] Start scanning\n")); auto networks = WiFi.scanNetworks(); if (networks == WIFI_SCAN_FAILED) { DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); return; } else if (0 == networks) { DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); return; } DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), networks); // SDK pre-allocates a memory region with the scan data, but the only API to get them is through this 'getter' method. // Pick them one by one and pass into the callback as our custom struct. wifi_scan_info_t info; for (int i = 0; i < networks; ++i) { WiFi.getNetworkInfo(i, info.ssid_scan, info.sec_scan, info.rssi_scan, info.BSSID_scan, info.chan_scan, info.hidden_scan ); snprintf_P(info.buffer, sizeof(info.buffer), PSTR("BSSID: %02X:%02X:%02X:%02X:%02X:%02X SEC: %s RSSI: %3d CH: %2d SSID: %s"), info.BSSID_scan[0], info.BSSID_scan[1], info.BSSID_scan[2], info.BSSID_scan[3], info.BSSID_scan[4], info.BSSID_scan[5], (info.sec_scan != ENC_TYPE_NONE ? "YES" : "NO "), info.rssi_scan, info.chan_scan, info.ssid_scan.c_str() ); callback(info); } WiFi.scanDelete(); } void _wifiCallback(justwifi_messages_t code, char * parameter) { if (MESSAGE_WPS_START == code) { _wifi_wps_running = true; return; } if (MESSAGE_SMARTCONFIG_START == code) { _wifi_smartconfig_running = true; return; } if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) { _wifi_wps_running = false; _wifi_smartconfig_running = false; return; } if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) { _wifi_wps_running = false; _wifi_smartconfig_running = false; const String current_ssid = WiFi.SSID(); const String current_pass = WiFi.psk(); // Write current ssid & pass at the end of the networks list unsigned char count; for (count = 0; count < WIFI_MAX_NETWORKS; count++) { const auto ssid = getSetting({"ssid", count}, _wifiSSID(count)); const auto pass = getSetting({"pass", count}, _wifiPass(count)); // Ignore existing network settings if (current_ssid.equals(ssid) && current_pass.equals(pass)) { return; } if (current_ssid.equals(ssid)) break; if (!ssid.length()) break; } // If we have reached the max we overwrite the first one if (WIFI_MAX_NETWORKS == count) count = 0; setSetting({"ssid", count}, current_ssid); setSetting({"pass", count}, current_pass); return; } } #if WIFI_AP_CAPTIVE #include "DNSServer.h" DNSServer _wifi_dnsServer; void _wifiCaptivePortal(justwifi_messages_t code, char * parameter) { if (MESSAGE_ACCESSPOINT_CREATED == code) { _wifi_dnsServer.setErrorReplyCode(DNSReplyCode::NoError); _wifi_dnsServer.start(53, "*", WiFi.softAPIP()); DEBUG_MSG_P(PSTR("[WIFI] Captive portal enabled\n")); } if (MESSAGE_CONNECTED == code) { _wifi_dnsServer.stop(); DEBUG_MSG_P(PSTR("[WIFI] Captive portal disabled\n")); } } #endif // WIFI_AP_CAPTIVE #if DEBUG_SUPPORT void _wifiDebugCallback(justwifi_messages_t code, char * parameter) { // ------------------------------------------------------------------------- if (code == MESSAGE_SCANNING) { DEBUG_MSG_P(PSTR("[WIFI] Scanning\n")); } if (code == MESSAGE_SCAN_FAILED) { DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); } if (code == MESSAGE_NO_NETWORKS) { DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); } if (code == MESSAGE_NO_KNOWN_NETWORKS) { DEBUG_MSG_P(PSTR("[WIFI] No known networks found\n")); } if (code == MESSAGE_FOUND_NETWORK) { DEBUG_MSG_P(PSTR("[WIFI] %s\n"), parameter); } // ------------------------------------------------------------------------- if (code == MESSAGE_CONNECTING) { DEBUG_MSG_P(PSTR("[WIFI] Connecting to %s\n"), parameter); } if (code == MESSAGE_CONNECT_WAITING) { // too much noise } if (code == MESSAGE_CONNECT_FAILED) { DEBUG_MSG_P(PSTR("[WIFI] Could not connect to %s\n"), parameter); } if (code == MESSAGE_CONNECTED) { wifiDebug(WIFI_STA); } if (code == MESSAGE_DISCONNECTED) { DEBUG_MSG_P(PSTR("[WIFI] Disconnected\n")); } // ------------------------------------------------------------------------- if (code == MESSAGE_ACCESSPOINT_CREATING) { DEBUG_MSG_P(PSTR("[WIFI] Creating access point\n")); } if (code == MESSAGE_ACCESSPOINT_CREATED) { wifiDebug(WIFI_AP); } if (code == MESSAGE_ACCESSPOINT_FAILED) { DEBUG_MSG_P(PSTR("[WIFI] Could not create access point\n")); } if (code == MESSAGE_ACCESSPOINT_DESTROYED) { _wifiUpdateSoftAP(); DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n")); } // ------------------------------------------------------------------------- if (code == MESSAGE_WPS_START) { DEBUG_MSG_P(PSTR("[WIFI] WPS started\n")); } if (code == MESSAGE_WPS_SUCCESS) { DEBUG_MSG_P(PSTR("[WIFI] WPS succeded!\n")); } if (code == MESSAGE_WPS_ERROR) { DEBUG_MSG_P(PSTR("[WIFI] WPS failed\n")); } // ------------------------------------------------------------------------ if (code == MESSAGE_SMARTCONFIG_START) { DEBUG_MSG_P(PSTR("[WIFI] Smart Config started\n")); } if (code == MESSAGE_SMARTCONFIG_SUCCESS) { DEBUG_MSG_P(PSTR("[WIFI] Smart Config succeded!\n")); } if (code == MESSAGE_SMARTCONFIG_ERROR) { DEBUG_MSG_P(PSTR("[WIFI] Smart Config failed\n")); } } #endif // DEBUG_SUPPORT // ----------------------------------------------------------------------------- // SETTINGS // ----------------------------------------------------------------------------- #if TERMINAL_SUPPORT void _wifiInitCommands() { terminalRegisterCommand(F("WIFI.STATIONS"), [](const terminal::CommandContext& ctx) { char buffer[64]; size_t stations = 0; auto* station = wifi_softap_get_station_info(); while (station) { sprintf_P(buffer, PSTR("[WIFI] %02X:%02X:%02X:%02X:%02X:%02X %s"), MAC2STR(station->bssid), IPAddress(station->ip.addr).toString().c_str() ); ctx.output.println(buffer); station = STAILQ_NEXT(station, next); ++stations; } wifi_softap_free_station_info(); if (!stations) { terminalError(ctx, F("No stations connected")); return; } terminalOK(ctx); }); terminalRegisterCommand(F("WIFI"), [](const terminal::CommandContext&) { wifiDebug(); terminalOK(); }); terminalRegisterCommand(F("WIFI.RESET"), [](const terminal::CommandContext&) { _wifiConfigure(); wifiDisconnect(); terminalOK(); }); terminalRegisterCommand(F("WIFI.STA"), [](const terminal::CommandContext&) { wifiStartSTA(); terminalOK(); }); terminalRegisterCommand(F("WIFI.AP"), [](const terminal::CommandContext&) { wifiStartAP(); terminalOK(); }); #if defined(JUSTWIFI_ENABLE_WPS) terminalRegisterCommand(F("WIFI.WPS"), [](const terminal::CommandContext&) { wifiStartWPS(); terminalOK(); }); #endif // defined(JUSTWIFI_ENABLE_WPS) #if defined(JUSTWIFI_ENABLE_SMARTCONFIG) terminalRegisterCommand(F("WIFI.SMARTCONFIG"), [](const terminal::CommandContext&) { wifiStartSmartConfig(); terminalOK(); }); #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG) terminalRegisterCommand(F("WIFI.SCAN"), [](const terminal::CommandContext&) { _wifiScan([](wifi_scan_info_t& info) { DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), info.buffer); }); terminalOK(); }); } #endif // ----------------------------------------------------------------------------- // WEB // ----------------------------------------------------------------------------- #if WEB_SUPPORT bool _wifiWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "wifi", 4) == 0) return true; if (strncmp(key, "ssid", 4) == 0) return true; if (strncmp(key, "pass", 4) == 0) return true; if (strncmp(key, "ip", 2) == 0) return true; if (strncmp(key, "gw", 2) == 0) return true; if (strncmp(key, "mask", 4) == 0) return true; if (strncmp(key, "dns", 3) == 0) return true; return false; } void _wifiWebSocketOnConnected(JsonObject& root) { root["wifiScan"] = getSetting("wifiScan", 1 == WIFI_SCAN_NETWORKS); JsonObject& wifi = root.createNestedObject("wifi"); root["max"] = WIFI_MAX_NETWORKS; const char* keys[] = { "ssid", "pass", "ip", "gw", "mask", "dns", "stored" }; JsonArray& schema = wifi.createNestedArray("schema"); schema.copyFrom(keys, 7); JsonArray& networks = wifi.createNestedArray("networks"); for (unsigned char index = 0; index < WIFI_MAX_NETWORKS; ++index) { if (!getSetting({"ssid", index}, _wifiSSID(index)).length()) break; JsonArray& network = networks.createNestedArray(); network.add(getSetting({"ssid", index}, _wifiSSID(index))); network.add(getSetting({"pass", index}, _wifiPass(index))); network.add(getSetting({"ip", index}, _wifiIP(index))); network.add(getSetting({"gw", index}, _wifiGateway(index))); network.add(getSetting({"mask", index}, _wifiNetmask(index))); network.add(getSetting({"dns", index}, _wifiDNS(index))); network.add(_wifiHasSSID(index)); } } void _wifiWebSocketScan(JsonObject& root) { JsonArray& scanResult = root.createNestedArray("scanResult"); _wifiScan([&scanResult](wifi_scan_info_t& info) { scanResult.add(info.buffer); }); } void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { if (strcmp(action, "scan") == 0) wsPost(client_id, _wifiWebSocketScan); } #endif // ----------------------------------------------------------------------------- // SUPPORT // ----------------------------------------------------------------------------- #if WIFI_GRATUITOUS_ARP_SUPPORT // ref: lwip src/core/netif.c netif_issue_reports(...) // ref: esp-lwip/core/ipv4/etharp.c garp_tmr() // TODO: only for ipv4, need (?) a different method with ipv6 bool _wifiSendGratuitousArp() { bool result = false; for (netif* interface = netif_list; interface != nullptr; interface = interface->next) { if ( (interface->flags & NETIF_FLAG_ETHARP) && (interface->hwaddr_len == ETHARP_HWADDR_LEN) #if LWIP_VERSION_MAJOR == 1 && (!ip_addr_isany(&interface->ip_addr)) #else && (!ip4_addr_isany_val(*netif_ip4_addr(interface))) #endif && (interface->flags & NETIF_FLAG_LINK_UP) && (interface->flags & NETIF_FLAG_UP) ) { etharp_gratuitous(interface); result = true; } } return result; } void _wifiSendGratuitousArp(unsigned long interval) { if (millis() - _wifi_gratuitous_arp_last > interval) { _wifi_gratuitous_arp_last = millis(); _wifiSendGratuitousArp(); } } #endif // WIFI_GRATUITOUS_ARP_SUPPORT // ----------------------------------------------------------------------------- // INFO // ----------------------------------------------------------------------------- // backported WiFiAPClass methods String _wifiRuntimeSoftApSsid() { struct softap_config config; wifi_softap_get_config(&config); char* name = reinterpret_cast(config.ssid); char ssid[sizeof(config.ssid) + 1]; memcpy(ssid, name, sizeof(config.ssid)); ssid[sizeof(config.ssid)] = '\0'; return String(ssid); } String _wifiRuntimeSoftApPass() { struct softap_config config; wifi_softap_get_config(&config); char* pass = reinterpret_cast(config.password); char psk[sizeof(config.password) + 1]; memcpy(psk, pass, sizeof(config.password)); psk[sizeof(config.password)] = '\0'; return String(psk); } void wifiDebug(WiFiMode_t modes) { #if DEBUG_SUPPORT bool footer = false; if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) { DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE STA\n")); DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.macAddress().c_str()); DEBUG_MSG_P(PSTR("[WIFI] GW %s\n"), WiFi.gatewayIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str()); DEBUG_MSG_P(PSTR("[WIFI] BSSID %s\n"), WiFi.BSSIDstr().c_str()); DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel()); DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI()); footer = true; } if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) { DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n")); DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), _wifiRuntimeSoftApSsid().c_str()); DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), _wifiRuntimeSoftApPass().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str()); footer = true; } if (WiFi.getMode() == 0) { DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE OFF\n")); DEBUG_MSG_P(PSTR("[WIFI] No connection\n")); footer = true; } if (footer) { DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n")); } #endif //DEBUG_SUPPORT } void wifiDebug() { wifiDebug(WIFI_AP_STA); } // ----------------------------------------------------------------------------- // API // ----------------------------------------------------------------------------- String getIP() { if (WiFi.getMode() == WIFI_AP) { return WiFi.softAPIP().toString(); } return WiFi.localIP().toString(); } String getNetwork() { if (WiFi.getMode() == WIFI_AP) { return jw.getAPSSID(); } return WiFi.SSID(); } bool wifiConnected() { return jw.connected(); } void wifiDisconnect() { jw.disconnect(); } void wifiStartSTA() { jw.disconnect(); jw.enableSTA(true); jw.enableAP(false); } void wifiStartAP(bool only) { if (only) { jw.enableSTA(false); jw.disconnect(); jw.resetReconnectTimeout(); } jw.enableAP(true); } void wifiStartAP() { wifiStartAP(true); } #if defined(JUSTWIFI_ENABLE_WPS) void wifiStartWPS() { jw.enableAP(false); jw.disconnect(); jw.startWPS(); } #endif // defined(JUSTWIFI_ENABLE_WPS) #if defined(JUSTWIFI_ENABLE_SMARTCONFIG) void wifiStartSmartConfig() { jw.enableAP(false); jw.disconnect(); jw.startSmartConfig(); } #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG) void wifiReconnectCheck() { bool connected = false; #if WEB_SUPPORT if (wsConnected()) connected = true; #endif #if TELNET_SUPPORT if (telnetConnected()) connected = true; #endif jw.enableSTA(true); jw.setReconnectTimeout(connected ? 0 : WIFI_RECONNECT_INTERVAL); } uint8_t wifiState() { uint8_t state = 0; if (jw.connected()) state += WIFI_STATE_STA; if (jw.connectable()) state += WIFI_STATE_AP; if (_wifi_wps_running) state += WIFI_STATE_WPS; if (_wifi_smartconfig_running) state += WIFI_STATE_SMARTCONFIG; return state; } void wifiRegister(wifi_callback_f callback) { jw.subscribe(callback); } WiFiApMode wifiApMode() { return _wifi_ap_mode; } // ----------------------------------------------------------------------------- // INITIALIZATION // ----------------------------------------------------------------------------- void wifiSetup() { // Backwards compat, we need to specify namespace moveSetting("apmode", "wifiApMode"); _wifiConfigure(); // Note that maximum amount of stations is set by `WiFi.softAP(...)` call, but justwifi handles that. // Default is 4, which we use here. However, maximum is 8. ref: // https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/soft-access-point-class.html#softap #if WIFI_AP_LEASES_SUPPORT wifiRegister([](justwifi_messages_t code, char*) { if (MESSAGE_ACCESSPOINT_CREATING != code) return; for (unsigned char index = 0; index < 4; ++index) { auto lease = getSetting({"wifiApLease", index}); if (12 != lease.length()) { break; } uint8_t mac[6] = {0}; if (!hexDecode(lease.c_str(), lease.length(), mac, sizeof(mac))) { break; } wifi_softap_add_dhcps_lease(mac); } }); #endif if (WiFiApMode::Enabled ==_wifi_ap_mode) { jw.enableAP(true); jw.enableSTA(true); } #if JUSTWIFI_ENABLE_SMARTCONFIG if (_wifi_smartconfig_initial) jw.startSmartConfig(); #endif // Message callbacks wifiRegister(_wifiCallback); #if WIFI_AP_CAPTIVE wifiRegister(_wifiCaptivePortal); #endif #if DEBUG_SUPPORT wifiRegister(_wifiDebugCallback); #endif #if WEB_SUPPORT wsRegister() .onAction(_wifiWebSocketOnAction) .onConnected(_wifiWebSocketOnConnected) .onKeyCheck(_wifiWebSocketOnKeyCheck); #endif #if TERMINAL_SUPPORT _wifiInitCommands(); #endif // Main callbacks espurnaRegisterLoop(wifiLoop); espurnaRegisterReload(_wifiConfigure); } void wifiLoop() { // Main wifi loop jw.loop(); // Process captrive portal DNS queries if in AP mode only #if WIFI_AP_CAPTIVE if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) { _wifi_dnsServer.processNextRequest(); } #endif // Only send out gra arp when in STA mode #if WIFI_GRATUITOUS_ARP_SUPPORT if (_wifi_gratuitous_arp_interval) { _wifiSendGratuitousArp(_wifi_gratuitous_arp_interval); } #endif // Check if we should disable AP static unsigned long last = 0; if (millis() - last > 60000) { last = millis(); _wifiCheckAP(); } }