diff --git a/code/espurna/alexa.cpp b/code/espurna/alexa.cpp index afde4d21..ffe3d000 100644 --- a/code/espurna/alexa.cpp +++ b/code/espurna/alexa.cpp @@ -208,8 +208,9 @@ void alexaSetup() { #endif // Register wifi callback - wifiRegister([](justwifi_messages_t code, char * parameter) { - if ((MESSAGE_CONNECTED == code) || (MESSAGE_DISCONNECTED == code)) { + wifiRegister([](wifi::Event event) { + if ((event == wifi::Event::StationConnected) + || (event == wifi::Event::StationDisconnected)) { _alexaConfigure(); } }); diff --git a/code/espurna/button.cpp b/code/espurna/button.cpp index 55601da1..bfa2c39a 100644 --- a/code/espurna/button.cpp +++ b/code/espurna/button.cpp @@ -453,11 +453,7 @@ void buttonEvent(size_t id, ButtonEvent event) { #endif case ButtonAction::AccessPoint: - if (wifiState() & WIFI_STATE_AP) { - wifiStartSTA(); - } else { - wifiStartAP(); - } + wifiToggleAp(); break; case ButtonAction::Reset: @@ -469,15 +465,9 @@ void buttonEvent(size_t id, ButtonEvent event) { break; case ButtonAction::Wps: -#if defined(JUSTWIFI_ENABLE_WPS) - wifiStartWPS(); -#endif break; case ButtonAction::SmartConfig: -#if defined(JUSTWIFI_ENABLE_SMARTCONFIG) - wifiStartSmartConfig(); -#endif break; case ButtonAction::BrightnessIncrease: diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 29d85a5a..40b42dbd 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -536,26 +536,46 @@ // WIFI // ----------------------------------------------------------------------------- -#ifndef WIFI_CONNECT_TIMEOUT -#define WIFI_CONNECT_TIMEOUT 60000 // Connecting timeout for WIFI in ms +#ifndef WIFI_CONNECT_RETRIES +#define WIFI_CONNECT_RETRIES 3 // Number of times before changing to the next configured network +#endif + +#ifndef WIFI_CONNECT_INTERVAL +#define WIFI_CONNECT_INTERVAL 3000 // Time (ms) between connection attempts #endif #ifndef WIFI_RECONNECT_INTERVAL -#define WIFI_RECONNECT_INTERVAL 180000 // If could not connect to WIFI, retry after this time in ms +#define WIFI_RECONNECT_INTERVAL 120000 // When all retries on all networks are exhausted, wait for this time (ms) and start from the beginning #endif #ifndef WIFI_MAX_NETWORKS -#define WIFI_MAX_NETWORKS 5 // Max number of WIFI connection configurations +#define WIFI_MAX_NETWORKS 5 // Maximum number of WiFi configurations in settings +#endif + +#ifndef WIFI_AP_CAPTIVE_SUPPORT +#define WIFI_AP_CAPTIVE_SUPPORT 1 // Captive portal for AP mode +#endif + +#ifndef WIFI_AP_CAPTIVE_ENABLED +#define WIFI_AP_CAPTIVE_ENABLED 1 // Enabled by default #endif -#ifndef WIFI_AP_CAPTIVE -#define WIFI_AP_CAPTIVE 1 // Captive portal enabled when in AP mode +#ifndef WIFI_STA_MODE +#define WIFI_STA_MODE wifi::StaMode::Enabled // By default, turn on STA interface and try to connect to configured networks + // - wifi::StaMode::Enabled (default) + // - wifi::StaMode::Disabled keeps STA disabled #endif #ifndef WIFI_AP_MODE -#define WIFI_AP_MODE WiFiApMode::Fallback // By default, fallback to AP mode if no STA connection - // Use WiFiApMode::Enabled to start it when the device boots - // Use WiFiApMode::Disabled to disable AP mode completely +#define WIFI_AP_MODE wifi::ApMode::Fallback // By default, enable AP if there is no STA connection + // - wifi::ApMode::Fallback (default) + // - wifi::ApMode::Enabled keeps AP enabled independent of STA + // - wifi::ApMode::Disabled keeps AP disabled +#endif + +#ifndef WIFI_FALLBACK_TIMEOUT +#define WIFI_FALLBACK_TIMEOUT 60000 // When AP is in FALLBACK mode and STA is connected, + // how long to wait until stopping the AP #endif #ifndef WIFI_AP_SSID @@ -573,15 +593,34 @@ // Use `set wifiApLease# MAC`, where MAC is a valid 12-byte HEX number without colons #endif +#ifndef WIFI_AP_CHANNEL +#define WIFI_AP_CHANNEL 1 +#endif + #ifndef WIFI_SLEEP_MODE #define WIFI_SLEEP_MODE WIFI_NONE_SLEEP // WIFI_NONE_SLEEP, WIFI_LIGHT_SLEEP or WIFI_MODEM_SLEEP #endif #ifndef WIFI_SCAN_NETWORKS -#define WIFI_SCAN_NETWORKS 1 // Perform a network scan before connecting +#define WIFI_SCAN_NETWORKS 1 // Perform a network scan before connecting and when RSSI threshold is reached #endif -// Optional hardcoded configuration (up to 5 networks, depending on WIFI_MAX_NETWORKS and espurna/wifi_config.h) +#ifndef WIFI_SCAN_RSSI_THRESHOLD +#define WIFI_SCAN_RSSI_THRESHOLD -73 // Consider current network for a reconnection cycle + // when it's RSSI value is below the specified threshold +#endif + +#ifndef WIFI_SCAN_RSSI_CHECKS +#define WIFI_SCAN_RSSI_CHECKS 3 // Amount of RSSI threshold checks before starting a scan +#endif + +#ifndef WIFI_SCAN_RSSI_CHECK_INTERVAL +#define WIFI_SCAN_RSSI_CHECK_INTERVAL 60000 // Time (ms) between RSSI checks +#endif + +// Optional hardcoded configuration +// NOTICE that these values become factory-defaults + #ifndef WIFI1_SSID #define WIFI1_SSID "" #endif @@ -702,14 +741,6 @@ #define WIFI5_DNS "" #endif -#ifndef WIFI_RSSI_1M -#define WIFI_RSSI_1M -30 // Calibrate it with your router reading the RSSI at 1m -#endif - -#ifndef WIFI_PROPAGATION_CONST -#define WIFI_PROPAGATION_CONST 4 // This is typically something between 2.7 to 4.3 (free space is 2) -#endif - // ref: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html#config-lwip-esp-gratuitous-arp // ref: https://github.com/xoseperez/espurna/pull/1877#issuecomment-525612546 // diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 7ea49fb8..948abd5a 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -5,16 +5,6 @@ #pragma once -// ----------------------------------------------------------------------------- -// WIFI -// ----------------------------------------------------------------------------- - -#define WIFI_STATE_AP 1 -#define WIFI_STATE_STA 2 -#define WIFI_STATE_AP_STA 3 -#define WIFI_STATE_WPS 4 -#define WIFI_STATE_SMARTCONFIG 8 - // ----------------------------------------------------------------------------- // GPIO // ----------------------------------------------------------------------------- diff --git a/code/espurna/led.cpp b/code/espurna/led.cpp index 19154524..4eadf41b 100644 --- a/code/espurna/led.cpp +++ b/code/espurna/led.cpp @@ -68,7 +68,6 @@ void LedPattern::stop() { // For network-based modes, cycle ON & OFF (time in milliseconds) // XXX: internals convert these to clock cycles, delay cannot be longer than 25000 / 50000 ms static const LedDelay _ledDelays[] { - {100, 100}, // Autoconfig {100, 4900}, // Connected {4900, 100}, // Connected (inverse) {100, 900}, // Config / AP @@ -76,19 +75,16 @@ static const LedDelay _ledDelays[] { {500, 500} // Idle }; -enum class LedDelayName { - NetworkAutoconfig, +enum class LedDelayName : int { NetworkConnected, NetworkConnectedInverse, NetworkConfig, NetworkConfigInverse, - NetworkIdle, - None + NetworkIdle }; -bool _led_update { false }; - std::vector _leds; +bool _led_update { false }; // ----------------------------------------------------------------------------- @@ -186,12 +182,17 @@ bool ledStatus(size_t id) { return false; } -const LedDelay& _ledDelayFromName(LedDelayName pattern) { - static_assert( - (sizeof(_ledDelays) / sizeof(_ledDelays[0])) <= static_cast(LedDelayName::None), - "Out-of-bounds" - ); - return _ledDelays[static_cast(pattern)]; +const LedDelay& _ledDelayFromName(LedDelayName name) { + switch (name) { + case LedDelayName::NetworkConnected: + case LedDelayName::NetworkConnectedInverse: + case LedDelayName::NetworkConfig: + case LedDelayName::NetworkConfigInverse: + case LedDelayName::NetworkIdle: + return _ledDelays[static_cast(name)]; + } + + return _ledDelays[static_cast(LedDelayName::NetworkIdle)]; } void _ledPattern(led_t& led) { @@ -359,83 +360,77 @@ void ledUpdate(bool do_update) { } void ledLoop() { - const auto wifi_state = wifiState(); - for (size_t id = 0; id < _leds.size(); ++id) { auto& led = _leds[id]; switch (led.mode()) { - case LED_MODE_MANUAL: - break; + case LED_MODE_MANUAL: + break; + + case LED_MODE_WIFI: + if (wifiConnected()) { + _ledBlink(led, LedDelayName::NetworkConnected); + } else if (wifiConnectable()) { + _ledBlink(led, LedDelayName::NetworkConfig); + } else { + _ledBlink(led, LedDelayName::NetworkIdle); + } + break; - case LED_MODE_WIFI: - if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { - _ledBlink(led, LedDelayName::NetworkAutoconfig); - } else if (wifi_state & WIFI_STATE_STA) { +#if RELAY_SUPPORT + + case LED_MODE_FINDME_WIFI: + if (wifiConnected()) { + if (relayStatus(_led_relays[id])) { _ledBlink(led, LedDelayName::NetworkConnected); - } else if (wifi_state & WIFI_STATE_AP) { + } else { + _ledBlink(led, LedDelayName::NetworkConnectedInverse); + } + } else if (wifiConnectable()) { + if (relayStatus(_led_relays[id])) { _ledBlink(led, LedDelayName::NetworkConfig); } else { - _ledBlink(led, LedDelayName::NetworkIdle); + _ledBlink(led, LedDelayName::NetworkConfigInverse); } + } else { + _ledBlink(led, LedDelayName::NetworkIdle); + } break; - #if RELAY_SUPPORT - - case LED_MODE_FINDME_WIFI: - if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { - _ledBlink(led, LedDelayName::NetworkAutoconfig); - } else if (wifi_state & WIFI_STATE_STA) { - if (relayStatus(_led_relays[id])) { - _ledBlink(led, LedDelayName::NetworkConnected); - } else { - _ledBlink(led, LedDelayName::NetworkConnectedInverse); - } - } else if (wifi_state & WIFI_STATE_AP) { - if (relayStatus(_led_relays[id])) { - _ledBlink(led, LedDelayName::NetworkConfig); - } else { - _ledBlink(led, LedDelayName::NetworkConfigInverse); - } + case LED_MODE_RELAY_WIFI: + if (wifiConnected()) { + if (relayStatus(_led_relays[id])) { + _ledBlink(led, LedDelayName::NetworkConnected); } else { - _ledBlink(led, LedDelayName::NetworkIdle); + _ledBlink(led, LedDelayName::NetworkConnectedInverse); } - break; - - case LED_MODE_RELAY_WIFI: - if ((wifi_state & WIFI_STATE_WPS) || (wifi_state & WIFI_STATE_SMARTCONFIG)) { - _ledBlink(led, LedDelayName::NetworkAutoconfig); - } else if (wifi_state & WIFI_STATE_STA) { - if (relayStatus(_led_relays[id])) { - _ledBlink(led, LedDelayName::NetworkConnected); - } else { - _ledBlink(led, LedDelayName::NetworkConnectedInverse); - } - } else if (wifi_state & WIFI_STATE_AP) { - if (relayStatus(_led_relays[id])) { - _ledBlink(led, LedDelayName::NetworkConfig); - } else { - _ledBlink(led, LedDelayName::NetworkConfigInverse); - } + } else if (wifiConnectable()) { + if (relayStatus(_led_relays[id])) { + _ledBlink(led, LedDelayName::NetworkConfig); } else { - _ledBlink(led, LedDelayName::NetworkIdle); + _ledBlink(led, LedDelayName::NetworkConfigInverse); } - break; + } else { + _ledBlink(led, LedDelayName::NetworkIdle); + } + break; - case LED_MODE_FOLLOW: - if (!_led_update) break; + case LED_MODE_FOLLOW: + if (_led_update) { _ledStatus(led, relayStatus(_led_relays[id])); - break; + } + break; - case LED_MODE_FOLLOW_INVERSE: - if (!_led_update) break; + case LED_MODE_FOLLOW_INVERSE: + if (_led_update) { led.status(!relayStatus(_led_relays[id])); _ledStatus(led, !relayStatus(_led_relays[id])); - break; + } + break; - case LED_MODE_FINDME: { - if (!_led_update) break; + case LED_MODE_FINDME: + if (_led_update) { bool status = true; for (size_t relayId = 0; relayId < relayCount(); ++relayId) { if (relayStatus(relayId)) { @@ -444,11 +439,11 @@ void ledLoop() { } } _ledStatus(led, status); - break; } + break; - case LED_MODE_RELAY: { - if (!_led_update) break; + case LED_MODE_RELAY: + if (_led_update) { bool status = false; for (size_t relayId = 0; relayId < relayCount(); ++relayId) { if (relayStatus(relayId)) { @@ -457,20 +452,22 @@ void ledLoop() { } } _ledStatus(led, status); - break; } + break; - #endif // RELAY_SUPPORT == 1 +#endif // RELAY_SUPPORT == 1 - case LED_MODE_ON: - if (!_led_update) break; + case LED_MODE_ON: + if (_led_update) { _ledStatus(led, true); - break; + } + break; - case LED_MODE_OFF: - if (!_led_update) break; + case LED_MODE_OFF: + if (_led_update) { _ledStatus(led, false); - break; + } + break; } diff --git a/code/espurna/led.h b/code/espurna/led.h index 6da0ebf6..6c1ebc12 100644 --- a/code/espurna/led.h +++ b/code/espurna/led.h @@ -47,15 +47,15 @@ struct LedDelay { LedDelay(on_ms, off_ms, 0) {} - LedDelayMode mode() const { + constexpr LedDelayMode mode() const { return _mode; } - unsigned long on() const { + constexpr unsigned long on() const { return _on; } - unsigned long off() const { + constexpr unsigned long off() const { return _off; } diff --git a/code/espurna/mdns.cpp b/code/espurna/mdns.cpp index 27a90fe1..03b06167 100644 --- a/code/espurna/mdns.cpp +++ b/code/espurna/mdns.cpp @@ -81,18 +81,24 @@ void mdnsServerSetup() { return; } - wifiRegister([](justwifi_messages_t code, char * parameter) { - if (code == MESSAGE_CONNECTED) { - _mdnsServerStart(); + // 2.7.4 and older require MDNS.begin() when interface is UP + // 3.0.0 and newer only need to do MDNS.begin() once at setup() + // (TODO: this is techically a constexpr, but not in 2.7.4 :/) + const static bool OldCore { esp8266::coreVersionNumeric() <= 20704000 }; + + wifiRegister([](wifi::Event event) { + if (event == wifi::Event::StationConnected) { #if MQTT_SUPPORT _mdnsFindMQTT(); #endif - } - - if (code == MESSAGE_ACCESSPOINT_CREATED) { + } else if (OldCore && (event == wifi::Event::Mode)) { _mdnsServerStart(); } }); + + if (!OldCore) { + _mdnsServerStart(); + } } #endif // MDNS_SERVER_SUPPORT diff --git a/code/espurna/mqtt.cpp b/code/espurna/mqtt.cpp index 660826a5..0632e7bb 100644 --- a/code/espurna/mqtt.cpp +++ b/code/espurna/mqtt.cpp @@ -642,7 +642,7 @@ bool _mqttHeartbeat(heartbeat::Mask mask) { mqttSend(MQTT_TOPIC_BSSID, WiFi.BSSIDstr().c_str()); if (mask & heartbeat::Report::Ip) - mqttSend(MQTT_TOPIC_IP, getIP().c_str()); + mqttSend(MQTT_TOPIC_IP, wifiStaIp().toString().c_str()); if (mask & heartbeat::Report::Mac) mqttSend(MQTT_TOPIC_MAC, WiFi.macAddress().c_str()); @@ -974,7 +974,7 @@ void mqttFlush() { root[MQTT_TOPIC_HOSTNAME] = getSetting("hostname", getIdentifier()); #endif #if MQTT_ENQUEUE_IP - root[MQTT_TOPIC_IP] = getIP(); + root[MQTT_TOPIC_IP] = wifiStaIp().toString(); #endif #if MQTT_ENQUEUE_MESSAGE_ID root[MQTT_TOPIC_MESSAGE_ID] = (Rtcmem->mqtt)++; @@ -1148,7 +1148,7 @@ void _mqttConnect() { if (_mqtt.connected() || (_mqtt_state != AsyncClientState::Disconnected)) return; // Do not connect if disabled or no WiFi - if (!_mqtt_enabled || (WiFi.status() != WL_CONNECTED)) return; + if (!_mqtt_enabled || (!wifiConnected())) return; // Check reconnect interval if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) return; diff --git a/code/espurna/telnet.cpp b/code/espurna/telnet.cpp index e2bb546f..1b928867 100644 --- a/code/espurna/telnet.cpp +++ b/code/espurna/telnet.cpp @@ -169,8 +169,8 @@ static std::vector _telnet_data_buffer; void _telnetDisconnect(unsigned char clientId) { _telnetClients[clientId]->stop(); _telnetClients[clientId] = nullptr; - wifiReconnectCheck(); DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId); + wifiApCheck(); } #elif TELNET_SERVER == TELNET_SERVER_ASYNC @@ -180,8 +180,8 @@ void _telnetCleanUp() { for (unsigned char clientId=0; clientId < TELNET_MAX_CLIENTS; ++clientId) { if (!_telnetClients[clientId]->connected()) { _telnetClients[clientId] = nullptr; - wifiReconnectCheck(); DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId); + wifiApCheck(); } } }); @@ -363,7 +363,6 @@ void _telnetNotifyConnected(unsigned char i) { // If there is no terminal support automatically dump info and crash data #if DEBUG_SUPPORT #if not TERMINAL_SUPPORT - wifiDebug(); crashDump(terminalDefaultStream()); crashClear(); #endif @@ -382,8 +381,6 @@ void _telnetNotifyConnected(unsigned char i) { _telnetClientsAuth[i] = true; } - wifiReconnectCheck(); - } #if TELNET_SERVER == TELNET_SERVER_WIFISERVER diff --git a/code/espurna/tuya.cpp b/code/espurna/tuya.cpp index b43f26fc..2168917d 100644 --- a/code/espurna/tuya.cpp +++ b/code/espurna/tuya.cpp @@ -166,11 +166,11 @@ namespace tuya { // -------------------------------------------- uint8_t getWiFiState() { - - uint8_t state = wifiState(); - if (state & WIFI_STATE_SMARTCONFIG) return 0x00; - if (state & WIFI_STATE_AP) return 0x01; - if (state & WIFI_STATE_STA) return 0x04; + if (wifiConnected()) { + return 0x04; + } else if (wifiConnectable()) { + return 0x01; + } return 0x02; } @@ -651,9 +651,12 @@ error: TUYA_SERIAL.begin(SerialSpeed); ::espurnaRegisterLoop(loop); - ::wifiRegister([](justwifi_messages_t code, char * parameter) { - if ((MESSAGE_CONNECTED == code) || (MESSAGE_DISCONNECTED == code)) { + ::wifiRegister([](wifi::Event event) { + switch (event) { + case wifi::Event::StationConnected: + case wifi::Event::StationDisconnected: sendWiFiStatus(); + break; } }); } diff --git a/code/espurna/wifi.cpp b/code/espurna/wifi.cpp index 30df14d6..9de83392 100644 --- a/code/espurna/wifi.cpp +++ b/code/espurna/wifi.cpp @@ -2,8 +2,12 @@ WIFI MODULE +Original code based on JustWifi, Wifi Manager for ESP8266 (GPLv3+) Copyright (C) 2016-2019 by Xose Pérez +Modified for ESPurna +Copyright (C) 2021 by Maxim Prokhorov + */ #include "wifi.h" @@ -12,554 +16,806 @@ Copyright (C) 2016-2019 by Xose Pérez #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; +#include -#if WIFI_GRATUITOUS_ARP_SUPPORT -unsigned long _wifi_gratuitous_arp_interval = 0; -unsigned long _wifi_gratuitous_arp_last = 0; +#if WIFI_AP_CAPTIVE_SUPPORT +#include #endif +#include +#include +#include +#include + // ----------------------------------------------------------------------------- -// PRIVATE +// SETTINGS // ----------------------------------------------------------------------------- namespace settings { namespace internal { -template <> -WiFiApMode convert(const String& value) { +template<> +wifi::StaMode convert(const String& value) { + return convert(value) + ? wifi::StaMode::Enabled + : wifi::StaMode::Disabled; +} + +template<> +wifi::ApMode convert(const String& value) { switch (value.toInt()) { case 0: - return WiFiApMode::Disabled; + return wifi::ApMode::Disabled; case 1: - return WiFiApMode::Enabled; - default: + return wifi::ApMode::Enabled; case 2: - return WiFiApMode::Fallback; + return wifi::ApMode::Fallback; } + + return wifi::build::softApMode(); } -template<> +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; + case 2: + return WIFI_MODEM_SLEEP; + case 1: + return WIFI_LIGHT_SLEEP; + case 0: + return WIFI_NONE_SLEEP; } -} -} // namespace internal -} // namespace settings - -String _wifiSettingsSoftApSsid() { - return getSetting("wifiApSsid", strlen(WIFI_AP_SSID) - ? F(WIFI_AP_SSID) - : getSetting("hostname", getIdentifier())); + return wifi::build::sleep(); } -String _wifiSettingsSoftApPass() { - return getSetting("wifiApPass", strlen(WIFI_AP_PASS) - ? F(WIFI_AP_PASS) - : getAdminPass()); +template <> +IPAddress convert(const String& value) { + IPAddress out; + out.fromString(value); + return out; } -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 - ); - } -} +// XXX: "(IP unset)" when not set, no point saving these :/ -void _wifiCheckAP() { - if ( - (WiFiApMode::Fallback == _wifi_ap_mode) - && ((WiFi.getMode() & WIFI_AP) > 0) - && jw.connected() - && (WiFi.softAPgetStationNum() == 0) - ) { - jw.enableAP(false); - } +String serialize(const IPAddress& ip) { + return ip.isSet() ? ip.toString() : emptyString; } -void _wifiConfigure() { - - jw.setHostname(getSetting("hostname", getIdentifier()).c_str()); - _wifiUpdateSoftAP(); - - jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); - wifiReconnectCheck(); +} // namespace internal +} // namespace settings - _wifi_ap_mode = getSetting("wifiApMode", WIFI_AP_MODE); +// ----------------------------------------------------------------------------- +// INTERNAL +// ----------------------------------------------------------------------------- - jw.enableAPFallback(_wifi_ap_mode != WiFiApMode::Disabled); - jw.cleanNetworks(); +namespace wifi { - // If system is flagged unstable we do not init wifi networks - #if SYSTEM_CHECK_ENABLED - if (!systemCheck()) return; - #endif +// XXX: esp8266 Arduino API inclues pseudo-modes and is not directly convertible +// into the SDK constants. Provide a constexpr version of the enum, since the code never +// actually uses `WiFi::mode(...)` directly, *but* opmode is retrieved using the SDK function. - 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)); +constexpr uint8_t OpmodeNull { NULL_MODE }; +constexpr uint8_t OpmodeSta { STATION_MODE }; +constexpr uint8_t OpmodeAp { SOFTAP_MODE }; +constexpr uint8_t OpmodeApSta { OpmodeSta | OpmodeAp }; - 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; - } +using Mac = std::array; +using Macs = std::vector; - bool result = false; +enum class ScanError { + None, + AlreadyScanning, + System, + NoNetworks +}; - 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()); - } +enum class Action { + StationConnect, + StationContinueConnect, + StationTryConnectBetter, + StationDisconnect, + AccessPointFallback, + AccessPointFallbackCheck, + AccessPointStart, + AccessPointStop, + TurnOff, + TurnOn +}; - if (!result) break; - } +using Actions = std::list; +using ActionsQueue = std::queue; + +enum class State { + Boot, + Connect, + TryConnectBetter, + Connected, + Idle, + Init, + Timeout, + Fallback, + WaitScan, + WaitScanWithoutCurrent, + WaitConnected +}; - #if JUSTWIFI_ENABLE_SMARTCONFIG - if (index == 0) _wifi_smartconfig_initial = true; - #endif +namespace internal { - jw.enableScan(getSetting("wifiScan", 1 == WIFI_SCAN_NETWORKS)); +// Module actions are controled in a serialzed manner, when internal loop is done with the +// current task and is free to take up another one. Allow to toggle OFF for the whole module, +// discarding any actions involving an active WiFi. Default is ON - const auto sleep_mode = getSetting("wifiSleep", WIFI_SLEEP_MODE); - WiFi.setSleepMode(sleep_mode); +bool enabled { true }; +ActionsQueue actions; - #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 +} // namespace internal - const auto tx_power = getSetting("wifiTxPwr", WIFI_OUTPUT_POWER_DBM); - WiFi.setOutputPower(tx_power); +uint8_t opmode() { + return wifi_get_opmode(); +} +bool enabled() { + return internal::enabled; } -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]; -}; +void enable() { + internal::enabled = true; +} -template -void _wifiScan(WiFiScanCallback callback) { - DEBUG_MSG_P(PSTR("[WIFI] Start scanning\n")); +void disable() { + internal::enabled = false; +} - 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; +void action(Action value) { + switch (value) { + case Action::StationConnect: + case Action::StationTryConnectBetter: + case Action::StationContinueConnect: + case Action::StationDisconnect: + case Action::AccessPointFallback: + case Action::AccessPointFallbackCheck: + case Action::AccessPointStart: + case Action::AccessPointStop: + if (!enabled()) { + return; + } + break; + case Action::TurnOff: + case Action::TurnOn: + break; } - DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), networks); + internal::actions.push(value); +} - // 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; +ActionsQueue& actions() { + return internal::actions; +} - for (int i = 0; i < networks; ++i) { +// ::forceSleepBegin() remembers the previous mode and ::forceSleepWake() calls station connect when it has STA in it :/ +// while we *do* set opmode to 0 to avoid this uncertainty, preper to call wake through SDK instead of the Arduino wrapper +// +// 0xFFFFFFF is a magic number per the NONOS API reference, 3.7.5 wifi_fpm_do_sleep: +// > If sleep_time_in_us is 0xFFFFFFF, the ESP8266 will sleep till be woke up as below: +// > • If wifi_fpm_set_sleep_type is set to be LIGHT_SLEEP_T, ESP8266 can wake up by GPIO. +// > • If wifi_fpm_set_sleep_type is set to be MODEM_SLEEP_T, ESP8266 can wake up by wifi_fpm_do_wakeup. +// +// In our case, wake-up is software driven, so the MODEM sleep is the only choice available. +// This version can *only* work from CONT context, since the only consumer atm is wifi::Action handler +// TODO(esp32): Null mode turns off radio, no need for these + +bool sleep() { + if ((opmode() == ::wifi::OpmodeNull) && (wifi_fpm_get_sleep_type() == NONE_SLEEP_T)) { + wifi_fpm_set_sleep_type(MODEM_SLEEP_T); + yield(); + wifi_fpm_open(); + yield(); + if (0 == wifi_fpm_do_sleep(0xFFFFFFF)) { + delay(10); + return true; + } + } - WiFi.getNetworkInfo(i, - info.ssid_scan, - info.sec_scan, - info.rssi_scan, - info.BSSID_scan, - info.chan_scan, - info.hidden_scan - ); + return false; +} - 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() - ); +bool wakeup() { + if (wifi_fpm_get_sleep_type() != NONE_SLEEP_T) { + wifi_fpm_do_wakeup(); + wifi_fpm_close(); + delay(10); + return true; + } - callback(info); + return false; +} +namespace debug { + +String error(wifi::ScanError error) { + const __FlashStringHelper* ptr { nullptr }; + + switch (error) { + case wifi::ScanError::AlreadyScanning: + ptr = F("Scan already in progress"); + break; + case wifi::ScanError::System: + ptr = F("Could not start the scan"); + break; + case wifi::ScanError::NoNetworks: + ptr = F("No networks"); + break; + case wifi::ScanError::None: + ptr = F("OK"); + break; } - WiFi.scanDelete(); + return ptr; +} +String ip(const IPAddress& addr) { + return addr.toString(); } -void _wifiCallback(justwifi_messages_t code, char * parameter) { +String ip(ip4_addr_t addr) { + String out; + out.reserve(16); - if (MESSAGE_WPS_START == code) { - _wifi_wps_running = true; - return; + bool delim { false }; + for (int byte = 0; byte < 4; ++byte) { + if (delim) { + out += '.'; + } + out += ip4_addr_get_byte_val(addr, byte); + delim = true; } - if (MESSAGE_SMARTCONFIG_START == code) { - _wifi_smartconfig_running = true; - return; + return out; +} + +String mac(const wifi::Mac& mac) { + String out; + out.reserve(18); + + bool delim { false }; + char buffer[3] = {0}; + for (auto& byte : mac) { + hexEncode(&byte, 1, buffer, sizeof(buffer)); + if (delim) { + out += ':'; + } + out += buffer; + delim = true; } - if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) { - _wifi_wps_running = false; - _wifi_smartconfig_running = false; - return; + return out; +} + +String authmode(AUTH_MODE mode) { + const __FlashStringHelper* ptr { F("UNKNOWN") }; + + switch (mode) { + case AUTH_OPEN: + ptr = F("OPEN"); + break; + case AUTH_WEP: + ptr = F("WEP"); + break; + case AUTH_WPA_PSK: + ptr = F("WPAPSK"); + break; + case AUTH_WPA2_PSK: + ptr = F("WPA2PSK"); + break; + case AUTH_WPA_WPA2_PSK: + ptr = F("WPAWPA2-PSK"); + break; + case AUTH_MAX: + break; } - if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) { - _wifi_wps_running = false; - _wifi_smartconfig_running = false; + return ptr; +} - const String current_ssid = WiFi.SSID(); - const String current_pass = WiFi.psk(); +String opmode(uint8_t mode) { + const __FlashStringHelper* ptr { nullptr }; + + switch (mode) { + case ::wifi::OpmodeApSta: + ptr = F("AP+STA"); + break; + case ::wifi::OpmodeSta: + ptr = F("STA"); + break; + case ::wifi::OpmodeAp: + ptr = F("AP"); + break; + case ::wifi::OpmodeNull: + ptr = F("NULL"); + break; + } - // 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; - } + return ptr; +} - // If we have reached the max we overwrite the first one - if (WIFI_MAX_NETWORKS == count) count = 0; +} // namespace debug - setSetting({"ssid", count}, current_ssid); - setSetting({"pass", count}, current_pass); +namespace settings { - return; +void migrate(int version) { + if (version && (version < 5)) { + moveSetting("apmode", "wifiApMode"); } +} +decltype(millis()) garpInterval() { + return getSetting("wifiGarpIntvl", secureRandom(wifi::build::garpIntervalMin(), wifi::build::garpIntervalMax())); } -#if WIFI_AP_CAPTIVE +float txPower() { + return getSetting("wifiTxPwr", wifi::build::outputDbm()); +} -#include "DNSServer.h" +WiFiSleepType_t sleep() { + return getSetting("wifiSleep", wifi::build::sleep()); +} -DNSServer _wifi_dnsServer; +bool scanNetworks() { + return getSetting("wifiScan", wifi::build::scanNetworks()); +} -void _wifiCaptivePortal(justwifi_messages_t code, char * parameter) { +int8_t scanRssiThreshold() { + return getSetting("wifiScanRssi", wifi::build::scanRssiThreshold()); +} - 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")); - } +String hostname() { + return getSetting("hostname", getIdentifier()); +} - if (MESSAGE_CONNECTED == code) { - _wifi_dnsServer.stop(); - DEBUG_MSG_P(PSTR("[WIFI] Captive portal disabled\n")); - } +wifi::StaMode staMode() { + return getSetting("wifiStaMode", wifi::build::staMode()); +} +IPAddress staIp(size_t index) { + return ::settings::internal::convert( + getSetting({"ip", index}, wifi::build::ip(index))); } -#endif // WIFI_AP_CAPTIVE +String staSsid(size_t index) { + return getSetting({"ssid", index}, wifi::build::ssid(index)); +} -#if DEBUG_SUPPORT +String staPassphrase(size_t index) { + return getSetting({"pass", index}, wifi::build::passphrase(index)); +} -void _wifiDebugCallback(justwifi_messages_t code, char * parameter) { +IPAddress staGateway(size_t index) { + return ::settings::internal::convert( + getSetting({"gw", index}, wifi::build::gateway(index))); +} - // ------------------------------------------------------------------------- +IPAddress staMask(size_t index) { + return ::settings::internal::convert( + getSetting({"mask", index}, wifi::build::mask(index))); +} - if (code == MESSAGE_SCANNING) { - DEBUG_MSG_P(PSTR("[WIFI] Scanning\n")); - } +IPAddress staDns(size_t index) { + return ::settings::internal::convert( + getSetting({"dns", index}, wifi::build::dns(index))); +} - if (code == MESSAGE_SCAN_FAILED) { - DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); - } +bool softApCaptive() { + return getSetting("wifiApCaptive", wifi::build::softApCaptive()); +} - if (code == MESSAGE_NO_NETWORKS) { - DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); - } +wifi::ApMode softApMode() { + return getSetting("wifiApMode", wifi::build::softApMode()); +} - if (code == MESSAGE_NO_KNOWN_NETWORKS) { - DEBUG_MSG_P(PSTR("[WIFI] No known networks found\n")); - } +String softApSsid() { + return getSetting("wifiApSsid", wifi::build::hasSoftApSsid() + ? wifi::build::softApSsid() + : hostname()); +} - if (code == MESSAGE_FOUND_NETWORK) { - DEBUG_MSG_P(PSTR("[WIFI] %s\n"), parameter); - } +String softApPassphrase() { + return getSetting("wifiApPass", wifi::build::hasSoftApPassphrase() + ? wifi::build::softApPassphrase() + : getAdminPass()); +} + +int8_t softApChannel() { + return getSetting("wifiApChannel", wifi::build::softApChannel()); +} - // ------------------------------------------------------------------------- +wifi::Mac softApLease(size_t index) { + wifi::Mac lease { 0u, 0u, 0u, 0u, 0u, 0u }; - if (code == MESSAGE_CONNECTING) { - DEBUG_MSG_P(PSTR("[WIFI] Connecting to %s\n"), parameter); + auto value = getSetting({"wifiApLease", index}); + if (12 == value.length()) { + hexDecode(value.c_str(), value.length(), lease.data(), lease.size()); } - if (code == MESSAGE_CONNECT_WAITING) { - // too much noise - } + return lease; +} - if (code == MESSAGE_CONNECT_FAILED) { - DEBUG_MSG_P(PSTR("[WIFI] Could not connect to %s\n"), parameter); - } +} // namespace settings - if (code == MESSAGE_CONNECTED) { - wifiDebug(WIFI_STA); - } +// We are guaranteed to have '\0' when <32 b/c the SDK zeroes out the data +// But, these are byte arrays, not C strings. When ssid_len is available, use it. +// When not, we are still expecting the <32 arrays to have '\0' at the end and we manually +// set the 32'nd char to '\0' to prevent conversion issues - if (code == MESSAGE_DISCONNECTED) { - DEBUG_MSG_P(PSTR("[WIFI] Disconnected\n")); - } +namespace { + +String convertSsid(const softap_config& config) { + String ssid; + ssid.concat(reinterpret_cast(config.ssid), config.ssid_len); + return ssid; +} + +String convertSsid(const bss_info& info) { + String ssid; + ssid.concat(reinterpret_cast(info.ssid), info.ssid_len); + return ssid; +} + +String convertSsid(const station_config& config) { + constexpr size_t SsidSize { sizeof(softap_config::ssid) }; + + const char* ptr { reinterpret_cast(config.ssid) }; + char ssid[SsidSize + 1]; + std::copy(ptr, ptr + SsidSize, ssid); + ssid[SsidSize] = '\0'; + + return ssid; +} + +template +String convertPassphrase(const T& config) { + const char* ptr { reinterpret_cast(config.password) }; - // ------------------------------------------------------------------------- + char passphrase[PassphraseSize + 1]; + std::copy(ptr, ptr + PassphraseSize, passphrase); + passphrase[PassphraseSize] = '\0'; - if (code == MESSAGE_ACCESSPOINT_CREATING) { - DEBUG_MSG_P(PSTR("[WIFI] Creating access point\n")); + return passphrase; +} + +template +wifi::Mac convertBssid(const T& info) { + wifi::Mac mac; + std::copy(info.bssid, info.bssid + 6, mac.begin()); + return mac; +} + +} // namespace + +struct Info { + Info() = default; + Info(const Info&) = default; + Info(Info&&) = default; + + Info(wifi::Mac&& bssid, AUTH_MODE authmode, int8_t rssi, uint8_t channel) : + _bssid(std::move(bssid)), + _authmode(authmode), + _rssi(rssi), + _channel(channel) + {} + + explicit Info(const bss_info& info) : + _bssid(convertBssid(info)), + _authmode(info.authmode), + _rssi(info.rssi), + _channel(info.channel) + {} + + Info& operator=(const Info&) = default; + Info& operator=(Info&&) = default; + + Info& operator=(const bss_info& info) { + _bssid = convertBssid(info); + _authmode = info.authmode; + _channel = info.channel; + _rssi = info.rssi; + return *this; } - if (code == MESSAGE_ACCESSPOINT_CREATED) { - wifiDebug(WIFI_AP); + explicit operator bool() const { + return _rssi != 0 && _channel != 0; } - if (code == MESSAGE_ACCESSPOINT_FAILED) { - DEBUG_MSG_P(PSTR("[WIFI] Could not create access point\n")); + bool operator<(const Info& rhs) const { + return _rssi < rhs._rssi; } - if (code == MESSAGE_ACCESSPOINT_DESTROYED) { - _wifiUpdateSoftAP(); - DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n")); + bool operator>(const Info& rhs) const { + return _rssi > rhs._rssi; } - // ------------------------------------------------------------------------- + const wifi::Mac& bssid() const { + return _bssid; + } - if (code == MESSAGE_WPS_START) { - DEBUG_MSG_P(PSTR("[WIFI] WPS started\n")); + AUTH_MODE authmode() const { + return _authmode; } - if (code == MESSAGE_WPS_SUCCESS) { - DEBUG_MSG_P(PSTR("[WIFI] WPS succeded!\n")); + int8_t rssi() const { + return _rssi; } - if (code == MESSAGE_WPS_ERROR) { - DEBUG_MSG_P(PSTR("[WIFI] WPS failed\n")); + uint8_t channel() const { + return _channel; } - // ------------------------------------------------------------------------ +private: + //Mac _bssid {{ 0u, 0u, 0u, 0u, 0u, 0u }}; // TODO: gcc4 can't figure out basic aggregate, replace when using gcc10 builds + Mac _bssid {}; + AUTH_MODE _authmode { AUTH_OPEN }; + int8_t _rssi { 0 }; + uint8_t _channel { 0u }; +}; + +struct SsidInfo { + SsidInfo() = delete; + + explicit SsidInfo(const bss_info& info) : + _ssid(convertSsid(info)), + _info(info) + {} - if (code == MESSAGE_SMARTCONFIG_START) { - DEBUG_MSG_P(PSTR("[WIFI] Smart Config started\n")); + SsidInfo(String&& ssid, wifi::Info&& info) : + _ssid(std::move(ssid)), + _info(std::move(info)) + {} + + const String& ssid() const { + return _ssid; } - if (code == MESSAGE_SMARTCONFIG_SUCCESS) { - DEBUG_MSG_P(PSTR("[WIFI] Smart Config succeded!\n")); + const wifi::Info& info() const { + return _info; } - if (code == MESSAGE_SMARTCONFIG_ERROR) { - DEBUG_MSG_P(PSTR("[WIFI] Smart Config failed\n")); + // decreasing order by rssi (default sort() order is increasing) + bool operator<(const SsidInfo& rhs) const { + if (!_info.rssi()) { + return false; + } + + return info() > rhs.info(); } -} +private: + String _ssid; + wifi::Info _info; +}; -#endif // DEBUG_SUPPORT +using SsidInfos = std::forward_list; -// ----------------------------------------------------------------------------- -// SETTINGS -// ----------------------------------------------------------------------------- +// Note that lwip config allows up to 3 DNS servers. But, most of the time we use DHCP. +// TODO: ::dns(size_t index)? how'd that look with settings? -#if TERMINAL_SUPPORT +struct IpSettings { + IpSettings() = default; + IpSettings(const IpSettings&) = default; + IpSettings(IpSettings&&) = default; -void _wifiInitCommands() { + IpSettings& operator=(const IpSettings&) = default; + IpSettings& operator=(IpSettings&&) = default; - terminalRegisterCommand(F("WIFI.STATIONS"), [](const terminal::CommandContext& ctx) { - char buffer[64]; + template + IpSettings(Ip&& ip, Gateway&& gateway, Netmask&& netmask, Dns&& dns) : + _ip(std::forward(ip)), + _gateway(std::forward(gateway)), + _netmask(std::forward(netmask)), + _dns(std::forward(dns)) + {} - size_t stations = 0; + const IPAddress& ip() const { + return _ip; + } - auto* station = wifi_softap_get_station_info(); + const IPAddress& gateway() const { + return _gateway; + } - 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; - } + const IPAddress& netmask() const { + return _netmask; + } - wifi_softap_free_station_info(); + const IPAddress& dns() const { + return _dns; + } - if (!stations) { - terminalError(ctx, F("No stations connected")); - return; - } + explicit operator bool() const { + return _ip.isSet() + && _gateway.isSet() + && _netmask.isSet() + && _dns.isSet(); + } +private: + IPAddress _ip; + IPAddress _gateway; + IPAddress _netmask; + IPAddress _dns; +}; - terminalOK(ctx); - }); +struct StaNetwork { + Mac bssid; + String ssid; + String passphrase; + int8_t rssi; + uint8_t channel; +}; - terminalRegisterCommand(F("WIFI"), [](const terminal::CommandContext&) { - wifiDebug(); - terminalOK(); - }); +struct SoftApNetwork { + Mac bssid; + String ssid; + String passphrase; + uint8_t channel; + AUTH_MODE authmode; +}; - terminalRegisterCommand(F("WIFI.RESET"), [](const terminal::CommandContext&) { - _wifiConfigure(); - wifiDisconnect(); - terminalOK(); - }); +struct Network { + Network() = delete; + Network(const Network&) = default; + Network(Network&&) = default; + + Network& operator=(Network&&) = default; + + template + explicit Network(Ssid&& ssid) : + _ssid(std::forward(ssid)) + {} + + template + Network(Ssid&& ssid, Passphrase&& passphrase) : + _ssid(std::forward(ssid)), + _passphrase(std::forward(passphrase)) + {} + + template + Network(Ssid&& ssid, Passphrase&& passphrase, Settings&& settings) : + _ssid(std::forward(ssid)), + _passphrase(std::forward(passphrase)), + _ipSettings(std::forward(settings)) + {} + + // TODO(?): in case SDK API is used directly, this also could use an authmode field + // Arduino wrapper sets WPAPSK minimum by default, so one use-case is to set it to WPA2PSK + + Network(Network&& other, wifi::Mac bssid, uint8_t channel) : + _ssid(std::move(other._ssid)), + _passphrase(std::move(other._passphrase)), + _ipSettings(std::move(other._ipSettings)), + _bssid(bssid), + _channel(channel) + {} + + bool dhcp() const { + return !_ipSettings; + } - terminalRegisterCommand(F("WIFI.STA"), [](const terminal::CommandContext&) { - wifiStartSTA(); - terminalOK(); - }); + const String& ssid() const { + return _ssid; + } - terminalRegisterCommand(F("WIFI.AP"), [](const terminal::CommandContext&) { - wifiStartAP(); - terminalOK(); - }); + const String& passphrase() const { + return _passphrase; + } - #if defined(JUSTWIFI_ENABLE_WPS) - terminalRegisterCommand(F("WIFI.WPS"), [](const terminal::CommandContext&) { - wifiStartWPS(); - terminalOK(); - }); - #endif // defined(JUSTWIFI_ENABLE_WPS) + const IpSettings& ipSettings() const { + return _ipSettings; + } - #if defined(JUSTWIFI_ENABLE_SMARTCONFIG) - terminalRegisterCommand(F("WIFI.SMARTCONFIG"), [](const terminal::CommandContext&) { - wifiStartSmartConfig(); - terminalOK(); - }); - #endif // defined(JUSTWIFI_ENABLE_SMARTCONFIG) + const wifi::Mac& bssid() const { + return _bssid; + } - terminalRegisterCommand(F("WIFI.SCAN"), [](const terminal::CommandContext&) { - _wifiScan([](wifi_scan_info_t& info) { - DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), info.buffer); - }); - terminalOK(); - }); + uint8_t channel() const { + return _channel; + } -} +private: + String _ssid; + String _passphrase; + IpSettings _ipSettings; -#endif + Mac _bssid {}; + uint8_t _channel { 0u }; +}; + +using Networks = std::list; // ----------------------------------------------------------------------------- -// WEB +// STATION // ----------------------------------------------------------------------------- -#if WEB_SUPPORT +namespace sta { -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; +constexpr auto ConnectionInterval = wifi::build::staConnectionInterval(); +constexpr auto ConnectionRetries = wifi::build::staConnectionRetries(); +constexpr auto ReconnectionInterval = wifi::build::staReconnectionInterval(); + +uint8_t channel() { + return wifi_get_channel(); +} + +int8_t rssi() { + return wifi_station_get_rssi(); } -void _wifiWebSocketOnConnected(JsonObject& root) { - root["wifiScan"] = getSetting("wifiScan", 1 == WIFI_SCAN_NETWORKS); +// Note that authmode is a spefific threshold selected by the Arduino WiFi.begin() +// (ref. Arduino ESP8266WiFi default, which is AUTH_WPA_WPA2_PSK in the current 3.0.0) +// Also, it is not really clear whether `wifi_get_channel()` will work correctly in the future versions, +// since the API seems to be related to the promiscuous WiFi (aka sniffer), but it does return the correct values. + +wifi::Info info(const station_config& config) { + return wifi::Info{ + convertBssid(config), + config.threshold.authmode, + rssi(), + channel()}; +} - JsonObject& wifi = root.createNestedObject("wifi"); - root["max"] = WIFI_MAX_NETWORKS; +wifi::Info info() { + station_config config{}; + wifi_station_get_config(&config); + return info(config); +} - const char* keys[] = { - "ssid", "pass", "ip", "gw", "mask", "dns", "stored" - }; - JsonArray& schema = wifi.createNestedArray("schema"); - schema.copyFrom(keys, 7); +wifi::IpSettings ipsettings() { + return { + WiFi.localIP(), + WiFi.gatewayIP(), + WiFi.subnetMask(), + WiFi.dnsIP()}; +} - JsonArray& networks = wifi.createNestedArray("networks"); +wifi::Mac bssid() { + station_config config{}; + wifi_station_get_config(&config); - 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)); - } + return convertBssid(config); } -void _wifiWebSocketScan(JsonObject& root) { - JsonArray& scanResult = root.createNestedArray("scanResult"); - _wifiScan([&scanResult](wifi_scan_info_t& info) { - scanResult.add(info.buffer); - }); +wifi::StaNetwork current(const station_config& config) { + return { + convertBssid(config), + convertSsid(config), + convertPassphrase(config), + rssi(), + channel()}; } -void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { - if (strcmp(action, "scan") == 0) wsPost(client_id, _wifiWebSocketScan); +wifi::StaNetwork current() { + station_config config{}; + wifi_station_get_config(&config); + return current(config); } -#endif +#if WIFI_GRATUITOUS_ARP_SUPPORT +namespace garp { +namespace internal { -// ----------------------------------------------------------------------------- -// SUPPORT -// ----------------------------------------------------------------------------- +Ticker timer; +bool wait { false }; +decltype(millis()) interval { wifi::build::garpIntervalMin() }; -#if WIFI_GRATUITOUS_ARP_SUPPORT +} // namespace internal -// 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 send() { + bool result { false }; - 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) ) { @@ -571,285 +827,1607 @@ bool _wifiSendGratuitousArp() { return result; } -void _wifiSendGratuitousArp(unsigned long interval) { - if (millis() - _wifi_gratuitous_arp_last > interval) { - _wifi_gratuitous_arp_last = millis(); - _wifiSendGratuitousArp(); +bool wait() { + if (internal::wait) { + return true; } + + internal::wait = true; + return false; } -#endif // WIFI_GRATUITOUS_ARP_SUPPORT +void stop() { + internal::timer.detach(); +} -// ----------------------------------------------------------------------------- -// INFO -// ----------------------------------------------------------------------------- +void start(decltype(millis()) ms) { + internal::timer.attach_ms(ms, []() { + internal::wait = false; + }); +} -// backported WiFiAPClass methods +} // namespace garp +#endif -String _wifiRuntimeSoftApSsid() { - struct softap_config config; - wifi_softap_get_config(&config); +namespace scan { - char* name = reinterpret_cast(config.ssid); - char ssid[sizeof(config.ssid) + 1]; - memcpy(ssid, name, sizeof(config.ssid)); - ssid[sizeof(config.ssid)] = '\0'; +using SsidInfosPtr = std::shared_ptr; - return String(ssid); -} +using Success = std::function; +using Error = std::function; -String _wifiRuntimeSoftApPass() { - struct softap_config config; - wifi_softap_get_config(&config); +struct Task { + Task() = delete; - char* pass = reinterpret_cast(config.password); - char psk[sizeof(config.password) + 1]; - memcpy(psk, pass, sizeof(config.password)); - psk[sizeof(config.password)] = '\0'; + template + Task(S&& success, E&& error) : + _success(std::forward(success)), + _error(std::forward(error)) + {} - return String(psk); -} + void success(bss_info* info) { + _success(info); + } -void wifiDebug(WiFiMode_t modes) { + void error(wifi::ScanError error) { + _error(error); + } - #if DEBUG_SUPPORT - bool footer = false; +private: + Success _success; + Error _error; +}; - if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) { +using TaskPtr = std::unique_ptr; - 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; +namespace internal { - } +TaskPtr task; - 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; - } +void stop() { + task = nullptr; +} - if (WiFi.getMode() == 0) { - DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE OFF\n")); - DEBUG_MSG_P(PSTR("[WIFI] No connection\n")); - footer = true; +// STATUS comes from c_types.h, and it seems this is the only place that uses it +// instead of some ESP-specific type. + +void complete(void* result, STATUS status) { + if (status) { // aka anything but OK / 0 + task->error(wifi::ScanError::System); + stop(); + return; } - if (footer) { - DEBUG_MSG_P(PSTR("[WIFI] ----------------------------------------------\n")); + size_t networks { 0ul }; + bss_info* head = reinterpret_cast(result); + for (bss_info* it = head; it; it = STAILQ_NEXT(it, next), ++networks) { + task->success(it); } - #endif //DEBUG_SUPPORT -} + if (!networks) { + task->error(wifi::ScanError::NoNetworks); + } -void wifiDebug() { - wifiDebug(WIFI_AP_STA); + stop(); } -// ----------------------------------------------------------------------------- -// API -// ----------------------------------------------------------------------------- +} // namespace internal -String getIP() { - if (WiFi.getMode() == WIFI_AP) { - return WiFi.softAPIP().toString(); +bool start(Success&& success, Error&& error) { + if (internal::task) { + error(wifi::ScanError::AlreadyScanning); + return false; } - return WiFi.localIP().toString(); -} -String getNetwork() { - if (WiFi.getMode() == WIFI_AP) { - return jw.getAPSSID(); - } - return WiFi.SSID(); -} + // Note that esp8266 callback only reports the resulting status and will (always?) timeout all by itself + // Default values are an active scan with some unspecified channel times. + // (zeroed out scan_config struct or simply nullptr) -bool wifiConnected() { - return jw.connected(); -} + // For example, c/p config from the current esp32 Arduino Core wrapper which are close to the values mentioned here: + // https://github.com/espressif/ESP8266_NONOS_SDK/issues/103#issuecomment-383440370 + // Which could be useful if scanning needs to be more aggressive or switched into PASSIVE scan type -void wifiDisconnect() { - jw.disconnect(); -} + //scan_config config{}; + //config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + //config.scan_time.active.min = 100; + //config.scan_time.active.max = 300; + + if (wifi_station_scan(nullptr, &internal::complete)) { + internal::task = std::make_unique(std::move(success), std::move(error)); + return true; + } -void wifiStartSTA() { - jw.disconnect(); - jw.enableSTA(true); - jw.enableAP(false); + error(wifi::ScanError::System); + return false; } -void wifiStartAP(bool only) { - if (only) { - jw.enableSTA(false); - jw.disconnect(); - jw.resetReconnectTimeout(); +// Alternative to the stock WiFi method, where we wait for the task to finish before returning +bool wait(Success&& success, Error&& error) { + auto result = start(std::move(success), std::move(error)); + while (internal::task) { + delay(100); } - jw.enableAP(true); -} -void wifiStartAP() { - wifiStartAP(true); + return result; } -#if defined(JUSTWIFI_ENABLE_WPS) -void wifiStartWPS() { - jw.enableAP(false); - jw.disconnect(); - jw.startWPS(); -} -#endif // defined(JUSTWIFI_ENABLE_WPS) +// Another alternative to the stock WiFi method, return a shared Info list +// Caller is expected to wait for the scan to complete before using the contents +SsidInfosPtr ssidinfos() { + auto infos = std::make_shared(); + + start( + [infos](bss_info* found) { + wifi::SsidInfo pair(*found); + infos->remove_if([&](const wifi::SsidInfo& current) { + return (current.ssid() == pair.ssid()) && (current.info() < pair.info()); + }); + infos->emplace_front(std::move(pair)); + }, + [infos](wifi::ScanError) { + infos->clear(); + }); -#if defined(JUSTWIFI_ENABLE_SMARTCONFIG) -void wifiStartSmartConfig() { - jw.enableAP(false); - jw.disconnect(); - jw.startSmartConfig(); + return infos; } -#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); -} +} // namespace scan -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; +bool enabled() { + return wifi::opmode() & wifi::OpmodeSta; } -void wifiRegister(wifi_callback_f callback) { - jw.subscribe(callback); -} +// XXX: WiFi.disconnect() also implicitly disables STA mode *and* erases the current STA config -WiFiApMode wifiApMode() { - return _wifi_ap_mode; +void disconnect() { + if (enabled()) { + wifi_station_disconnect(); + } } -void wifiTurnOff() { - jw.turnOff(); +// Some workarounds for built-in WiFi management: +// - don't *intentionally* perist current SSID & PASS even when persistance is disabled from the Arduino Core side. +// while this seems like a good idea in theory, we end up with a bunch of async actions coming our way. +// - station disconnect events are linked with the connection routine as well, single WiFi::begin() may trigger up to +// 3 events (as observed with `WiFi::waitForConnectResult()`) before the connection loop stops further attempts +// - explicit OPMODE changes to both notify the userspace when the change actually happens (alternative is SDK event, but it is SYS context), +// since *all* STA & AP start-up methods will implicitly change the mode (`WiFi.begin()`, `WiFi.softAP()`, `WiFi.config()`) + +void enable() { + if (WiFi.enableSTA(true)) { + disconnect(); + ETS_UART_INTR_DISABLE(); + wifi_station_set_reconnect_policy(false); + if (wifi_station_get_auto_connect()) { + wifi_station_set_auto_connect(false); + } + ETS_UART_INTR_ENABLE(); + return; + } + + // `std::abort()` calls are the to ensure the mode actually changes, but it should be extremely rare + // it may be also wise to add these for when the mode is already the expected one, + // since we should enforce mode changes to happen *only* through the configuration loop + abort(); } -void wifiTurnOn() { - jw.turnOn(); +void disable() { + if (!WiFi.enableSTA(false)) { + abort(); + } } -// ----------------------------------------------------------------------------- -// INITIALIZATION -// ----------------------------------------------------------------------------- +namespace connection { +namespace internal { -void wifiSetup() { +struct Task { + using Iterator = wifi::Networks::iterator; + + Task() = delete; + Task(const Task&) = delete; + Task(Task&&) = delete; + + explicit Task(String&& hostname, Networks&& networks, int retries) : + _hostname(std::move(hostname)), + _networks(std::move(networks)), + _begin(_networks.begin()), + _end(_networks.end()), + _current(_begin), + _retries(retries), + _retry(_retries) + {} + + bool empty() const { + return _networks.empty(); + } - // Backwards compat, we need to specify namespace - moveSetting("apmode", "wifiApMode"); + size_t count() const { + return _networks.size(); + } - jw.begin(); - _wifiConfigure(); + bool done() const { + return _current == _end; + } - // 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; + bool next() { + if (!done()) { + if (_retry-- < 0) { + _retry = _retries; + _current = std::next(_current); + } + return !done(); + } + + return false; + } + + // Sanity checks for SSID & PASSPHRASE lengths are performed by the WiFi.begin() + // (or, failing connection, if we ever use raw SDK API) + + bool connect() const { + if (!done() && wifi::sta::enabled()) { + wifi::sta::disconnect(); + auto& network = *_current; + if (!network.dhcp()) { + auto& ipsettings = network.ipSettings(); + if (!WiFi.config(ipsettings.ip(), ipsettings.gateway(), ipsettings.netmask(), ipsettings.dns())) { + return false; + } + } + + // Only the STA cares about the hostname setting + // esp8266 specific Arduino-specific - this sets lwip internal structs related to the DHCPc + WiFi.hostname(_hostname); + + if (network.channel()) { + WiFi.begin(network.ssid(), network.passphrase(), + network.channel(), network.bssid().data()); + } else { + WiFi.begin(network.ssid(), network.passphrase()); + } + + return true; + } + + return false; + } + + Networks& networks() { + return _networks; + } + + void reset() { + _begin = _networks.begin(); + _end = _networks.end(); + _current = _begin; + _retry = _retries; + } - for (unsigned char index = 0; index < 4; ++index) { - auto lease = getSetting({"wifiApLease", index}); - if (12 != lease.length()) { + // Since after sort() the ssid<->info pairs will be in a proper order, look up the known network and move it to the front aka 'head' + // Continue after shifting the 'head' element one element further, b/c we also a guaranteed that ssid<->info pairs are unique + // Authmode comparison is pretty lenient, so only requirement is availability of the passphrase text. + + // Does not invalidate iterators, since the elements are swapped in-place, but we still need to reset to initial state. + + void sort(scan::SsidInfosPtr&& ptr) { + auto& pairs = *ptr; + pairs.sort(); + + auto begin = _networks.begin(); + auto end = _networks.end(); + + auto head = begin; + + for (auto& pair : pairs) { + for (auto network = head; (head != end) && (network != end); ++network) { + if (pair.ssid() != (*network).ssid()) { + continue; + } + + auto& info = pair.info(); + if ((*network).passphrase().length() + && (info.authmode() == AUTH_OPEN)) { + continue; + } + + *network = wifi::Network(std::move(*network), info.bssid(), info.channel()); + if (network != head) { + std::swap(*network, *head); + } + ++head; break; } + } + + reset(); + } + + // Allow to remove the currently used network right from the scan routine + // Only makes sense when wifi::Network's bssid exist, either after sort() or if loaded from settings + + bool filter(const wifi::Info& info) { + _networks.remove_if([&](const wifi::Network& network) { + return network.bssid() == info.bssid(); + }); + reset(); + return !done(); + } + + +private: + String _hostname; + Networks _networks; + Iterator _begin; + Iterator _end; + Iterator _current; + + const int _retries; + int _retry; +}; + +station_status_t last { STATION_IDLE }; +bool connected { false }; + +Ticker timer; +bool persist { false }; +bool lock { false }; + +using TaskPtr = std::unique_ptr; +TaskPtr task; + +} // namespace internal + +bool locked() { + return internal::lock; +} + +void unlock() { + internal::lock = false; +} + +void lock() { + internal::lock = true; +} + +void persist(bool value) { + internal::persist = value; +} + +bool persist() { + return internal::persist; +} + +void stop() { + if (!locked()) { + internal::task.reset(); + internal::timer.detach(); + } +} + +bool started() { + return static_cast(internal::task); +} + +void start(String&& hostname, Networks&& networks, int retries) { + if (!locked()) { + internal::task = std::make_unique( + std::move(hostname), + std::move(networks), + retries); + internal::timer.detach(); + } +} + +void schedule(decltype(millis()) ms, wifi::Action next) { + internal::timer.once_ms(ms, [next]() { + wifi::action(next); + unlock(); + }); + lock(); +} + +void continued() { + schedule(wifi::sta::ConnectionInterval, wifi::Action::StationContinueConnect); +} + +void initial() { + schedule(wifi::sta::ReconnectionInterval, wifi::Action::StationConnect); +} + +bool next() { + return internal::task->next(); +} + +bool connect() { + return internal::task->connect(); +} + +bool filter(const wifi::Info& info) { + return internal::task->filter(info); +} + +void sort(scan::SsidInfosPtr&& infos) { + internal::task->sort(std::move(infos)); +} + +station_status_t last() { + return internal::last; +} + +// Note that `wifi_station_get_connect_status()` only makes sence when something is setting `wifi_set_event_handler_cb(...)` +// *and*, it should only be expected to work when STA is not yet connected. After a successful connection, we should track the network interface and / or SDK events. +// Events are already enabled in the Arduino Core (and heavily wired through-out it, so we can't override b/c only one handler is allowed). + +bool wait() { + internal::last = wifi_station_get_connect_status(); + bool out { false }; + + switch (internal::last) { + case STATION_CONNECTING: + out = true; + break; + case STATION_GOT_IP: + internal::connected = true; + break; + case STATION_IDLE: + case STATION_NO_AP_FOUND: + case STATION_CONNECT_FAIL: + case STATION_WRONG_PASSWORD: + break; + } + + return out; +} + +// TODO(Core 2.7.4): `WiFi.isConnected()` is a simple `wifi_station_get_connect_status() == STATION_GOT_IP`, +// Meaning, it will never detect link up / down updates when AP silently kills the connection or something else unexpected happens. +// Running JustWiFi with autoconnect + reconnect enabled, it silently avoided the issue b/c the SDK reconnect routine disconnected the STA, +// causing our state machine to immediatly cancel it (since `WL_CONNECTED != WiFi.status()`) and then try to connect again using it's own loop. +// We could either (* is used currently): +// - (*) listen for the SDK event through the `WiFi.onStationModeDisconnected()` +// - ( ) poll NETIF_FLAG_LINK_UP for the lwip's netif, since the SDK will bring the link down on disconnection +// find the `interface` in the `netif_list`, where `interface->num == STATION_IF` +// - ( ) use lwip's netif event system from the recent Core, track UP and DOWN for a specific interface number +// this one is probably only used internally, thus should be treated as a private API +// - ( ) poll whether `wifi_get_ip_info(STATION_IF, &ip);` is set to something valid +// (tuple of ip, gw and mask) +// - ( ) poll `WiFi.localIP().isSet()` +// (will be unset when the link is down) + +// placing status into a simple bool to avoid extracting ip info every time someone needs to check the connection + +bool connected() { + return internal::connected; +} + +bool connecting() { + return static_cast(internal::task); +} + +bool lost() { + static bool last { internal::connected }; + + bool out { false }; + if (internal::connected != last) { + last = internal::connected; + if (!last) { + if (persist() && !connecting()) { + schedule(wifi::sta::ConnectionInterval * wifi::sta::ConnectionRetries, wifi::Action::StationConnect); + } + out = true; + } + } + + return out; +} + +} // namespace connection + +bool connected() { + return connection::connected(); +} + +bool connecting() { + return connection::connecting(); +} + +bool scanning() { + return static_cast(scan::internal::task); +} + +// TODO: generic onEvent is deprecated on esp8266 in favour of the event-specific +// methods returning 'cancelation' token. Right now it is a basic shared_ptr with an std function inside of it. +// esp32 only has a generic onEvent, but event names are not compatible with the esp8266 version. + +void init() { + static auto status = WiFi.onStationModeDisconnected([](const auto& src) { + connection::internal::connected = false; + }); + disconnect(); + disable(); +} + +void toggle() { + auto current = enabled(); + connection::persist(!current); + wifi::action(current + ? wifi::Action::StationDisconnect + : wifi::Action::StationConnect); +} + +namespace scan { +namespace periodic { +namespace internal { + +constexpr int8_t Checks { wifi::build::scanRssiChecks() }; +constexpr decltype(millis()) CheckInterval { wifi::build::scanRssiCheckInterval() }; + +int8_t threshold { wifi::build::scanRssiThreshold() }; +int8_t counter { Checks }; +Ticker timer; + +void task() { + if (!wifi::sta::connected()) { + counter = Checks; + return; + } + + auto rssi = wifi::sta::rssi(); + if (rssi > threshold) { + counter = Checks; + } else if (rssi < threshold) { + if (counter < 0) { + return; + } + + if (!--counter) { + wifi::action(wifi::Action::StationTryConnectBetter); + } + } +} + +void start() { + counter = Checks; + timer.attach_ms(CheckInterval, task); +} + +void stop() { + counter = Checks; + timer.detach(); +} + +} // namespace internal + +void threshold(int8_t value) { + internal::threshold = value; +} + +int8_t threshold() { + return internal::threshold; +} + +void stop() { + internal::stop(); +} + +void start() { + internal::start(); +} - uint8_t mac[6] = {0}; - if (!hexDecode(lease.c_str(), lease.length(), mac, sizeof(mac))) { +bool check() { + if (internal::counter <= 0) { + internal::counter = internal::Checks; + return true; + } + + return false; +} + +bool enabled() { + return internal::timer.active(); +} + +} // namespace periodic +} // namespace scan +} // namespace sta + +// ----------------------------------------------------------------------------- +// ACCESS POINT +// ----------------------------------------------------------------------------- + +namespace ap { + +static constexpr size_t LeasesMax { 4u }; + +namespace internal { + +#if WIFI_AP_CAPTIVE_SUPPORT +bool captive { wifi::build::softApCaptive() }; +DNSServer dns; +#endif + +#if WIFI_AP_LEASES_SUPPORT +wifi::Macs leases; +#endif + +} // namespace internal + +#if WIFI_AP_CAPTIVE_SUPPORT + +void captive(bool value) { + internal::captive = value; +} + +bool captive() { + return internal::captive; +} + +void dnsLoop() { + internal::dns.processNextRequest(); +} + +#endif + +void enable() { + if (!WiFi.enableAP(true)) { + abort(); + } +} + +void disable() { + if (!WiFi.enableAP(false)) { + abort(); + } +} + +bool enabled() { + return wifi::opmode() & WIFI_AP; +} + +void toggle() { + wifi::action(wifi::ap::enabled() + ? wifi::Action::AccessPointStop + : wifi::Action::AccessPointStart); +} + +#if WIFI_AP_LEASES_SUPPORT + +void setupLeases() { + for (auto& lease : internal::leases) { + wifi_softap_add_dhcps_lease(lease.data()); + } +} + +void clearLeases() { + internal::leases.clear(); +} + +template +void lease(T&& mac) { + if (internal::leases.size() < LeasesMax) { + internal::leases.push_back(std::forward(mac)); + } +} + +#endif + +void stop() { +#if WIFI_AP_CAPTIVE_SUPPORT + internal::dns.stop(); +#endif + WiFi.softAPdisconnect(); +} + +void start(String&& ssid, String&& passphrase, uint8_t channel) { + if (!enabled()) { + return; + } + + if (!ssid.length()) { + disable(); + return; + } + +#if WIFI_AP_LEASES_SUPPORT + // Default amount of stations is 4, which we use here b/c softAp is called without arguments. + // When chaging the number below, update LeasesMax / use it as the 5th param + // (4th is `hidden` SSID) + setupLeases(); +#endif + // TODO: softAP() implicitly enables AP mode + enable(); + WiFi.softAP(ssid, passphrase, channel); + +#if WIFI_AP_CAPTIVE_SUPPORT + if (internal::captive) { + internal::dns.setErrorReplyCode(DNSReplyCode::NoError); + internal::dns.start(53, "*", WiFi.softAPIP()); + } else { + internal::dns.stop(); + } +#endif +} + +wifi::SoftApNetwork current() { + softap_config config{}; + wifi_softap_get_config(&config); + + wifi::Mac mac; + WiFi.softAPmacAddress(mac.data()); + + return { + mac, + convertSsid(config), + convertPassphrase(config), + config.channel, + config.authmode}; +} + +void init() { + disable(); +} + +uint8_t stations() { + return WiFi.softAPgetStationNum(); +} + +namespace fallback { +namespace internal { + +bool enabled { false }; +decltype(millis()) timeout { wifi::build::softApFallbackTimeout() }; +Ticker timer; + +} // namespace internal + +void enable() { + internal::enabled = true; +} + +void disable() { + internal::enabled = false; +} + +bool enabled() { + return internal::enabled; +} + +void remove() { + internal::timer.detach(); +} + +bool scheduled() { + return internal::timer.active(); +} + +void check(); + +void schedule() { + internal::timer.once_ms(internal::timeout, check); +} + +void check() { + if (wifi::ap::enabled() + && wifi::sta::connected() + && !wifi::ap::stations()) + { + remove(); + wifi::action(wifi::Action::AccessPointStop); + return; + } + + schedule(); +} + +} // namespace fallback +} // namespace ap + +// ----------------------------------------------------------------------------- +// SETTINGS +// ----------------------------------------------------------------------------- + +namespace settings { + +wifi::Networks networks() { + wifi::Networks out; + for (size_t id = 0; id < wifi::build::NetworksMax; ++id) { + auto ssid = wifi::settings::staSsid(id); + if (!ssid.length()) { + break; + } + + auto pass = wifi::settings::staPassphrase(id); + auto ip = staIp(id); + if (ip.isSet()) { + out.emplace_back(std::move(ssid), std::move(pass), + wifi::IpSettings{std::move(ip), staGateway(id), staMask(id), staDns(id)}); + } else { + out.emplace_back(std::move(ssid), std::move(pass)); + } + } + + auto leftover = std::unique(out.begin(), out.end(), [](const wifi::Network& lhs, const wifi::Network& rhs) { + return lhs.ssid() == rhs.ssid(); + }); + out.erase(leftover, out.end()); + + return out; +} + +void configure() { + auto ap_mode = wifi::settings::softApMode(); + if (wifi::ApMode::Fallback == ap_mode) { + wifi::ap::fallback::enable(); + } else { + wifi::ap::fallback::disable(); + wifi::ap::fallback::remove(); + wifi::action((ap_mode == wifi::ApMode::Enabled) + ? wifi::Action::AccessPointStart + : wifi::Action::AccessPointStop); + } + +#if WIFI_AP_CAPTIVE_SUPPORT + wifi::ap::captive(wifi::settings::softApCaptive()); +#endif +#if WIFI_AP_LEASES_SUPPORT + wifi::ap::clearLeases(); + for (size_t index = 0; index < wifi::ap::LeasesMax; ++index) { + wifi::ap::lease(wifi::settings::softApLease(index)); + } +#endif + + auto sta_enabled = (wifi::StaMode::Enabled == wifi::settings::staMode()); + wifi::sta::connection::persist(sta_enabled); + wifi::action(sta_enabled + ? wifi::Action::StationConnect + : wifi::Action::StationDisconnect); + + wifi::sta::scan::periodic::threshold(wifi::settings::scanRssiThreshold()); + +#if WIFI_GRATUITOUS_ARP_SUPPORT + wifi::sta::garp::start(wifi::settings::garpInterval()); +#endif + + WiFi.setSleepMode(wifi::settings::sleep()); + WiFi.setOutputPower(wifi::settings::txPower()); +} + +} // namespace settings + +// ----------------------------------------------------------------------------- +// TERMINAL +// ----------------------------------------------------------------------------- + +namespace terminal { + +#if TERMINAL_SUPPORT + +void init() { + + terminalRegisterCommand(F("WIFI.STATIONS"), [](const ::terminal::CommandContext& ctx) { + size_t stations { 0ul }; + for (auto* it = wifi_softap_get_station_info(); it; it = STAILQ_NEXT(it, next), ++stations) { + ctx.output.printf_P(PSTR("%s %s\n"), + wifi::debug::mac(convertBssid(*it)).c_str(), + wifi::debug::ip(it->ip).c_str()); + } + + wifi_softap_free_station_info(); + + if (!stations) { + terminalError(ctx, F("No stations connected")); + return; + } + + terminalOK(ctx); + }); + + terminalRegisterCommand(F("NETWORK"), [](const ::terminal::CommandContext& ctx) { + for (auto& addr : addrList) { + ctx.output.printf_P(PSTR("%s%d %4s %6s "), + addr.ifname().c_str(), + addr.ifnumber(), + addr.ifUp() ? "up" : "down", + addr.isLocal() ? "local" : "global"); + +#if LWIP_IPV6 + if (addr.isV4()) { +#endif + ctx.output.printf_P(PSTR("ip %s gateway %s mask %s\n"), + wifi::debug::ip(addr.ipv4()).c_str(), + wifi::debug::ip(addr.gw()).c_str(), + wifi::debug::ip(addr.netmask()).c_str()); +#if LWIP_IPV6 + } else { + // TODO: ip6_addr[...] array is included in the list + // we'll just see another entry + // TODO: routing info is not attached to the netif :/ + // ref. nd6.h (and figure out what it does) + ctx.output.printf_P(PSTR("ip %s\n"), + wifi::debug::ip(netif->ip6_addr[i]).c_str()); + } +#endif + + } + + for (int n = 0; n < DNS_MAX_SERVERS; ++n) { + auto ip = IPAddress(dns_getserver(n)); + if (!ip.isSet()) { break; } + ctx.output.printf_P(PSTR("dns %s\n"), wifi::debug::ip(ip).c_str()); + } + }); + + terminalRegisterCommand(F("WIFI"), [](const ::terminal::CommandContext& ctx) { + const auto mode = wifi::opmode(); + ctx.output.printf_P(PSTR("OPMODE: %s\n"), wifi::debug::opmode(mode).c_str()); + + if (mode & OpmodeAp) { + auto current = wifi::ap::current(); - wifi_softap_add_dhcps_lease(mac); + ctx.output.printf_P(PSTR("SoftAP: bssid %s channel %hhu auth %s ssid \"%s\" passphrase \"%s\"\n"), + wifi::debug::mac(current.bssid).c_str(), + current.channel, + wifi::debug::authmode(current.authmode).c_str(), + current.ssid.c_str(), + current.passphrase.c_str()); } + + if (mode & OpmodeSta) { + if (wifi::sta::connected()) { + station_config config{}; + wifi_station_get_config(&config); + + auto network = wifi::sta::current(config); + ctx.output.printf_P(PSTR("STA: bssid %s rssi %hhd channel %hhu ssid \"%s\"\n"), + wifi::debug::mac(network.bssid).c_str(), + network.rssi, network.channel, network.ssid.c_str()); + } else { + ctx.output.println(F("STA: disconnected")); + } + } + + terminalOK(ctx); + }); + + terminalRegisterCommand(F("WIFI.RESET"), [](const ::terminal::CommandContext& ctx) { + wifiDisconnect(); + wifi::settings::configure(); + terminalOK(ctx); + }); + + terminalRegisterCommand(F("WIFI.STA"), [](const ::terminal::CommandContext& ctx) { + wifi::sta::toggle(); + terminalOK(ctx); + }); + + terminalRegisterCommand(F("WIFI.AP"), [](const ::terminal::CommandContext& ctx) { + wifi::ap::toggle(); + terminalOK(ctx); + }); + + terminalRegisterCommand(F("WIFI.SCAN"), [](const ::terminal::CommandContext& ctx) { + wifi::sta::scan::wait( + [&](bss_info* info) { + ctx.output.printf_P(PSTR("BSSID: %s AUTH: %11s RSSI: %3hhd CH: %2hhu SSID: %s\n"), + wifi::debug::mac(convertBssid(*info)).c_str(), + wifi::debug::authmode(info->authmode).c_str(), + info->rssi, + info->channel, + convertSsid(*info).c_str() + ); + }, + [&](wifi::ScanError error) { + terminalError(ctx, wifi::debug::error(error)); + } + ); + }); + +} + +} // namespace terminal + +#endif + +// ----------------------------------------------------------------------------- +// WEB +// ----------------------------------------------------------------------------- + +namespace web { + +#if WEB_SUPPORT + +bool onKeyCheck(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 onConnected(JsonObject& root) { + root["wifiScan"] = wifi::settings::scanNetworks(); + + JsonObject& wifi = root.createNestedObject("wifiConfig"); + root["max"] = wifi::build::NetworksMax; + + { + const char* schema_keys[] = { + "ssid", + "pass", + "ip", + "gw", + "mask", + "dns" + }; + + JsonArray& schema = wifi.createNestedArray("schema"); + schema.copyFrom(schema_keys, sizeof(schema_keys) / sizeof(*schema_keys)); + } + + JsonArray& networks = wifi.createNestedArray("networks"); + + // TODO: send build flags as 'original' replacements? + // with the current model, removing network from the UI is + // equivalent to the factory reset and will silently use the build default + auto entries = wifi::settings::networks(); + for (auto& entry : entries) { + JsonArray& network = networks.createNestedArray(); + + network.add(entry.ssid()); + network.add(entry.passphrase()); + + auto& ipsettings = entry.ipSettings(); + network.add(::settings::internal::serialize(ipsettings.ip())); + network.add(::settings::internal::serialize(ipsettings.gateway())); + network.add(::settings::internal::serialize(ipsettings.netmask())); + network.add(::settings::internal::serialize(ipsettings.dns())); + } +} + +void onScan(uint32_t client_id) { + if (wifi::sta::scanning()) { + return; + } + + wifi::sta::scan::start([client_id](bss_info* found) { + wifi::SsidInfo result(*found); + wsPost(client_id, [result](JsonObject& root) { + JsonArray& scan = root.createNestedArray("scanResult"); + scan.add(result.ssid()); + + auto& info = result.info(); + scan.add(info.rssi()); + scan.add(info.authmode()); + scan.add(info.channel()); + scan.add(wifi::debug::mac(info.bssid())); + }); + }, + [client_id](wifi::ScanError error) { + wsPost(client_id, [error](JsonObject& root) { + root["scanError"] = wifi::debug::error(error); + }); }); +} + +void onAction(uint32_t client_id, const char* action, JsonObject&) { + if (strcmp(action, "scan") == 0) { + onScan(client_id); + } +} + #endif - if (WiFiApMode::Enabled ==_wifi_ap_mode) { - jw.enableAP(true); - jw.enableSTA(true); +} // namespace web + +// ----------------------------------------------------------------------------- +// INITIALIZATION +// ----------------------------------------------------------------------------- + +namespace debug { + +String event(wifi::Event value) { + String out; + + switch (value) { + case wifi::Event::Initial: + out = F("Initial"); + break; + case wifi::Event::Mode: { + const auto mode = wifi::opmode(); + out = F("Mode changed to "); + out += wifi::debug::opmode(mode); + break; + } + case wifi::Event::StationInit: + out = F("Station init"); + break; + case wifi::Event::StationScan: + out = F("Scanning"); + break; + case wifi::Event::StationConnecting: + out = F("Connecting"); + break; + case wifi::Event::StationConnected: { + auto current = wifi::sta::current(); + out += F("Connected to BSSID "); + out += wifi::debug::mac(current.bssid); + out += F(" SSID "); + out += current.ssid; + break; + } + case wifi::Event::StationTimeout: + out = F("Connection timeout"); + break; + case wifi::Event::StationDisconnected: { + auto current = wifi::sta::current(); + out += F("Disconnected from "); + out += current.ssid; + break; + } + case wifi::Event::StationReconnect: + out = F("Reconnecting"); + break; } - #if JUSTWIFI_ENABLE_SMARTCONFIG - if (_wifi_smartconfig_initial) jw.startSmartConfig(); - #endif + return out; +} + +const char* state(wifi::State value) { + switch (value) { + case wifi::State::Boot: + return "Boot"; + case wifi::State::Connect: + return "Connect"; + case wifi::State::TryConnectBetter: + return "TryConnectBetter"; + case wifi::State::Fallback: + return "Fallback"; + case wifi::State::Connected: + return "Connected"; + case wifi::State::Idle: + return "Idle"; + case wifi::State::Init: + return "Init"; + case wifi::State::Timeout: + return "Timeout"; + case wifi::State::WaitScan: + return "WaitScan"; + case wifi::State::WaitScanWithoutCurrent: + return "WaitScanWithoutCurrent"; + case wifi::State::WaitConnected: + return "WaitConnected"; + } - // Message callbacks - wifiRegister(_wifiCallback); - #if WIFI_AP_CAPTIVE - wifiRegister(_wifiCaptivePortal); - #endif - #if DEBUG_SUPPORT - wifiRegister(_wifiDebugCallback); - #endif + return ""; +} - #if WEB_SUPPORT - wsRegister() - .onAction(_wifiWebSocketOnAction) - .onConnected(_wifiWebSocketOnConnected) - .onKeyCheck(_wifiWebSocketOnKeyCheck); - #endif +} // namespace debug - #if TERMINAL_SUPPORT - _wifiInitCommands(); - #endif +namespace internal { - // Main callbacks - espurnaRegisterLoop(wifiLoop); - espurnaRegisterReload(_wifiConfigure); +// STA + AP FALLBACK: +// - try connection +// - if ok, stop existing AP +// - if not, keep / start AP +// +// STA: +// - try connection +// - don't do anything on completion +// +// TODO? WPS / SMARTCONFIG + STA + AP FALLBACK +// - same as above +// - when requested, make sure there are no active connections +// abort when sta connected or ap is connected +// - run autoconf, receive credentials and store in a free settings slot + +// TODO: provide a clearer 'unroll' of the current state? + +using EventCallbacks = std::forward_list; +EventCallbacks callbacks; + +void publish(wifi::Event event) { + for (auto& callback : callbacks) { + callback(event); + } +} +void subscribe(wifi::EventCallback callback) { + callbacks.push_front(callback); } -void wifiLoop() { +namespace { + +} // namespace - // Main wifi loop - jw.loop(); +State handleAction(State& state, Action action) { + switch (action) { + case Action::StationConnect: + if (!wifi::sta::connecting() && !wifi::sta::connected()) { + if (!wifi::sta::enabled()) { + wifi::sta::enable(); + publish(wifi::Event::Mode); + } + + if (!wifi::sta::connecting()) { + state = State::Init; + } + } + break; + + case Action::StationContinueConnect: + if (wifi::sta::connecting() && !wifi::sta::connection::locked()) { + state = State::Connect; + } + break; + + case Action::StationDisconnect: + if (wifi::sta::connected()) { + wifi::ap::fallback::remove(); + wifi::sta::disconnect(); + } - // Process captrive portal DNS queries if in AP mode only - #if WIFI_AP_CAPTIVE - if ((WiFi.getMode() & WIFI_AP) == WIFI_AP) { - _wifi_dnsServer.processNextRequest(); + if (wifi::sta::connecting()) { + wifi::sta::connection::unlock(); + wifi::sta::connection::stop(); } - #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); + if (wifi::sta::enabled()) { + wifi::sta::disable(); + publish(wifi::Event::Mode); } - #endif + break; - // Check if we should disable AP - static unsigned long last = 0; - if (millis() - last > 60000) { - last = millis(); - _wifiCheckAP(); + case Action::StationTryConnectBetter: + if (!wifi::sta::connected() || wifi::sta::connecting()) { + wifi::sta::scan::periodic::stop(); + break; + } + + if (wifi::sta::scan::periodic::check()) { + state = State::TryConnectBetter; + } + break; + + case Action::AccessPointFallback: + case Action::AccessPointStart: + if (!wifi::ap::enabled()) { + wifi::ap::enable(); + wifi::ap::start( + wifi::settings::softApSsid(), + wifi::settings::softApPassphrase(), + wifi::settings::softApChannel()); + if ((Action::AccessPointFallback == action) + && wifi::ap::fallback::enabled()) { + wifi::ap::fallback::schedule(); + } + } + break; + + case Action::AccessPointFallbackCheck: + if (wifi::ap::fallback::enabled()) { + wifi::ap::fallback::check(); + } + break; + + case Action::AccessPointStop: + if (wifi::ap::enabled()) { + wifi::ap::fallback::remove(); + wifi::ap::stop(); + wifi::ap::disable(); + publish(wifi::Event::Mode); + } + break; + + case Action::TurnOff: + if (wifi::enabled()) { + wifi::ap::fallback::remove(); + wifi::ap::stop(); + wifi::ap::disable(); + wifi::sta::scan::periodic::stop(); + wifi::sta::connection::stop(); + wifi::sta::disconnect(); + wifi::sta::disable(); + wifi::disable(); + if (!wifi::sleep()) { + wifi::action(wifi::Action::TurnOn); + break; + } + } + break; + + case Action::TurnOn: + if (!wifi::enabled()) { + wifi::enable(); + wifi::wakeup(); + wifi::settings::configure(); + } + break; + + } + + return state; +} + +bool prepareConnection() { + if (wifi::sta::enabled()) { + auto networks = wifi::settings::networks(); + if (networks.size()) { + wifi::sta::connection::start( + wifi::settings::hostname(), std::move(networks), wifi::sta::ConnectionRetries); + return true; + } + } + + return false; +} + +void loop() { + static decltype(wifi::sta::scan::ssidinfos()) infos; + + static State state { State::Boot }; + static State last_state { state }; + + if (last_state != state) { + DEBUG_MSG_P(PSTR("[WIFI] State %s -> %s\n"), + debug::state(last_state), + debug::state(state)); + last_state = state; + } + + switch (state) { + + case State::Boot: + publish(wifi::Event::Initial); + state = State::Idle; + break; + + case State::Init: { + if (!prepareConnection()) { + state = State::Fallback; + break; + } + + wifi::sta::scan::periodic::stop(); + if (wifi::settings::scanNetworks()) { + infos = wifi::sta::scan::ssidinfos(); + state = State::WaitScan; + break; + } + state = State::Connect; + break; + } + + case State::TryConnectBetter: + if (wifi::settings::scanNetworks() && prepareConnection()) { + wifi::sta::scan::periodic::stop(); + infos = wifi::sta::scan::ssidinfos(); + state = State::WaitScanWithoutCurrent; + break; + } + state = State::Idle; + break; + + case State::Fallback: + publish(wifi::Event::StationReconnect); + wifi::sta::connection::initial(); + wifi::action(wifi::Action::AccessPointFallback); + state = State::Idle; + break; + + case State::WaitScan: + if (wifi::sta::scanning()) { + break; + } + + wifi::sta::connection::sort(std::move(infos)); + state = State::Connect; + break; + + case State::WaitScanWithoutCurrent: + if (wifi::sta::scanning()) { + break; + } + + wifi::sta::connection::sort(std::move(infos)); + if (wifi::sta::connection::filter(wifi::sta::info())) { + wifi::sta::disconnect(); + state = State::Connect; + break; + } + + state = State::Idle; + break; + + case State::Connect: { + if (wifi::sta::connection::connect()) { + state = State::WaitConnected; + publish(wifi::Event::StationConnecting); + } else { + state = State::Timeout; + } + break; + } + + case State::WaitConnected: + if (wifi::sta::connection::wait()) { + break; + } + + if (wifi::sta::connected()) { + state = State::Connected; + break; + } + + state = State::Timeout; + break; + + // Current logic closely follows the SDK connection routine with reconnect enabled, + // and will retry the same network multiple times before giving up. + case State::Timeout: + wifi::sta::connection::unlock(); + if (wifi::sta::connecting() && wifi::sta::connection::next()) { + wifi::sta::connection::continued(); + state = State::Idle; + publish(wifi::Event::StationTimeout); + } else { + wifi::sta::connection::stop(); + state = State::Fallback; + } + break; + + case State::Connected: + infos.reset(); + wifi::sta::connection::unlock(); + wifi::sta::connection::stop(); + if (wifi::settings::scanNetworks()) { + wifi::sta::scan::periodic::start(); + } + state = State::Idle; + publish(wifi::Event::StationConnected); + break; + + case State::Idle: { + auto& actions = wifi::actions(); + if (!actions.empty()) { + state = handleAction(state, actions.front()); + actions.pop(); + } + break; } + } + + // SDK disconnection event is specific to the phy layer. i.e. it will happen all the same + // when trying to connect and being unable to find the AP, being forced out by the AP with bad credentials + // or being disconnected when the wireless signal is lost. + // Thus, provide a specific connected -> disconnected event specific to the IP network availability. + if (wifi::sta::connection::lost()) { + publish(wifi::Event::StationDisconnected); + } + +#if WIFI_AP_CAPTIVE_SUPPORT + // Captive portal only queues packets and those need to be processed asap + if (wifi::ap::enabled() && wifi::ap::captive()) { + wifi::ap::dnsLoop(); + } +#endif +#if WIFI_GRATUITOUS_ARP_SUPPORT + // ref: https://github.com/xoseperez/espurna/pull/1877#issuecomment-525612546 + // Periodically send out ARP, even if no one asked + if (wifi::sta::connected() && !wifi::sta::garp::wait()) { + wifi::sta::garp::send(); + } +#endif +} + +// XXX: With Arduino Core 3.0.0, WiFi is asleep on boot +// It will wake up when calling WiFi::mode(...): +// - WiFi.begin(...) +// - WiFi.softAP(...) +// - WiFi.enableSTA(...) +// - WiFi.enableAP(...) +// ref. https://github.com/esp8266/Arduino/pull/7902 + +void init() { + WiFi.persistent(false); + wifi::ap::init(); + wifi::sta::init(); +} + +} // namespace internal +} // namespace wifi + +// ----------------------------------------------------------------------------- +// API +// ----------------------------------------------------------------------------- + +void wifiRegister(wifi::EventCallback callback) { + wifi::internal::subscribe(callback); +} + +bool wifiConnectable() { + return wifi::ap::enabled(); +} + +bool wifiConnected() { + return wifi::sta::connected(); +} + +IPAddress wifiStaIp() { + if (wifi::opmode() & wifi::OpmodeSta) { + return WiFi.localIP(); + } + + return {}; +} + +String wifiStaSsid() { + if (wifi::opmode() & wifi::OpmodeSta) { + auto current = wifi::sta::current(); + return current.ssid; + } + + return emptyString; +} + +void wifiDisconnect() { + wifi::sta::disconnect(); +} + +void wifiToggleAp() { + wifi::ap::toggle(); +} + +void wifiToggleSta() { + wifi::sta::toggle(); +} + +void wifiTurnOff() { + wifi::action(wifi::Action::TurnOff); +} + +void wifiTurnOn() { + wifi::action(wifi::Action::TurnOn); +} + +void wifiApCheck() { + wifi::action(wifi::Action::AccessPointFallbackCheck); +} + +void wifiSetup() { + wifi::internal::init(); + wifi::settings::migrate(migrateVersion()); + wifi::settings::configure(); + +#if SYSTEM_CHECK_ENABLED + if (!systemCheck()) { + wifi::actions() = wifi::ActionsQueue{}; + wifi::action(wifi::Action::AccessPointStart); + } +#endif + +#if DEBUG_SUPPORT + wifiRegister([](wifi::Event event) { + DEBUG_MSG_P(PSTR("[WIFI] %s\n"), wifi::debug::event(event).c_str()); + }); +#endif + +#if WEB_SUPPORT + wsRegister() + .onAction(wifi::web::onAction) + .onConnected(wifi::web::onConnected) + .onKeyCheck(wifi::web::onKeyCheck); +#endif + +#if TERMINAL_SUPPORT + wifi::terminal::init(); +#endif + + espurnaRegisterLoop(wifi::internal::loop); + espurnaRegisterReload(wifi::settings::configure); } diff --git a/code/espurna/wifi.h b/code/espurna/wifi.h index cb911d96..c32e45c4 100644 --- a/code/espurna/wifi.h +++ b/code/espurna/wifi.h @@ -20,7 +20,7 @@ Copyright (C) 2016-2019 by Xose Pérez // (HACK) allow us to use internal lwip struct. // esp8266 re-defines enum values from tcp header... include them first #define LWIP_INTERNAL -#include +#include #include #undef LWIP_INTERNAL @@ -40,35 +40,65 @@ extern "C" { #define TCP_MSS (1460) #endif -using wifi_callback_f = void(*)(justwifi_messages_t code, char * parameter); +namespace wifi { + +enum class Event { + Initial, // aka boot + Mode, // when opmode changes + StationInit, // station initialized by the connetion routine + StationScan, // scanning before the connection + StationConnecting, // network was selected and connection is in progress + StationConnected, // successful connection + StationDisconnected, // disconnected from the current network + StationTimeout, // timeout after the previous connecting state + StationReconnect // timeout after all connection loops failed +}; + +using EventCallback = void(*)(Event event); + +enum class StaMode { + Disabled, + Enabled +}; -enum class WiFiApMode { +enum class ApMode { Disabled, Enabled, Fallback }; -uint8_t wifiState(); -void wifiReconnectCheck(); +} // namespace wifi + +// Note that 'connected' status is *only* for the WiFi STA. +// Overall connectivity depends on low-level network stack and it may be +// useful to check whether relevant interfaces are up and have a routable IP +// instead of exclusively depending on the WiFi API. +// (e.g. when we have injected ethernet, wireguard, etc. interfaces. +// esp8266 implementation specifically uses lwip, ref. `netif_list`) bool wifiConnected(); -String getNetwork(); -String getIP(); +// Whether the AP is up and running +bool wifiConnectable(); + +// Current STA connection +String wifiStaSsid(); +IPAddress wifiStaIp(); -void wifiDebug(); -void wifiDebug(WiFiMode_t modes); +// Request to change the current STA / AP status +// Current state persists until reset or configuration reload +void wifiToggleAp(); +void wifiToggleSta(); -WiFiApMode wifiApMode(); -void wifiStartAP(); -void wifiStartSTA(); +// Disconnects STA intefrace +// (and will immediatly trigger a reconnection) void wifiDisconnect(); + +// Toggle WiFi modem void wifiTurnOff(); void wifiTurnOn(); -void wifiStartWPS(); -void wifiStartSmartConfig(); - -void wifiRegister(wifi_callback_f callback); +// Trigger fallback check for the AP +void wifiApCheck(); +void wifiRegister(wifi::EventCallback); void wifiSetup(); -void wifiLoop(); diff --git a/code/espurna/wifi_config.h b/code/espurna/wifi_config.h index 9d08ef62..3d794fb3 100644 --- a/code/espurna/wifi_config.h +++ b/code/espurna/wifi_config.h @@ -10,7 +10,92 @@ Copyright (C) 2020 by Maxim Prokhorov #include "espurna.h" -constexpr bool _wifiHasSSID(unsigned char index) { +namespace wifi { +namespace build { + +constexpr size_t NetworksMax { WIFI_MAX_NETWORKS }; + +constexpr unsigned long staReconnectionInterval() { + return WIFI_RECONNECT_INTERVAL; +} + +constexpr unsigned long staConnectionInterval() { + return WIFI_CONNECT_INTERVAL; +} + +constexpr int staConnectionRetries() { + return WIFI_CONNECT_RETRIES; +} + +constexpr wifi::StaMode staMode() { + return WIFI_STA_MODE; +} + +constexpr bool softApCaptive() { + return 1 == WIFI_AP_CAPTIVE_ENABLED; +} + +constexpr wifi::ApMode softApMode() { + return WIFI_AP_MODE; +} + +constexpr uint8_t softApChannel() { + return WIFI_AP_CHANNEL; +} + +constexpr bool hasSoftApSsid() { + return strlen(WIFI_AP_SSID); +} + +const __FlashStringHelper* softApSsid() { + return F(WIFI_AP_SSID); +} + +constexpr bool hasSoftApPassphrase() { + return strlen(WIFI_AP_PASS); +} + +const __FlashStringHelper* softApPassphrase() { + return F(WIFI_AP_PASS); +} + +constexpr unsigned long softApFallbackTimeout() { + return WIFI_FALLBACK_TIMEOUT; +} + +constexpr bool scanNetworks() { + return 1 == WIFI_SCAN_NETWORKS; +} + +constexpr int8_t scanRssiThreshold() { + return WIFI_SCAN_RSSI_THRESHOLD; +} + +constexpr unsigned long scanRssiCheckInterval() { + return WIFI_SCAN_RSSI_CHECK_INTERVAL; +} + +constexpr int8_t scanRssiChecks() { + return WIFI_SCAN_RSSI_CHECKS; +} + +constexpr unsigned long garpIntervalMin() { + return WIFI_GRATUITOUS_ARP_INTERVAL_MIN; +} + +constexpr unsigned long garpIntervalMax() { + return WIFI_GRATUITOUS_ARP_INTERVAL_MAX; +} + +constexpr WiFiSleepType_t sleep() { + return WIFI_SLEEP_MODE; +} + +constexpr float outputDbm() { + return WIFI_OUTPUT_POWER_DBM; +} + +constexpr bool hasSsid(size_t index) { return ( (index == 0) ? (strlen(WIFI1_SSID) > 0) : (index == 1) ? (strlen(WIFI2_SSID) > 0) : @@ -20,7 +105,7 @@ constexpr bool _wifiHasSSID(unsigned char index) { ); } -constexpr bool _wifiHasIP(unsigned char index) { +constexpr bool hasIp(size_t index) { return ( (index == 0) ? (strlen(WIFI1_IP) > 0) : (index == 1) ? (strlen(WIFI2_IP) > 0) : @@ -30,7 +115,7 @@ constexpr bool _wifiHasIP(unsigned char index) { ); } -const __FlashStringHelper* _wifiSSID(unsigned char index) { +const __FlashStringHelper* ssid(size_t index) { return ( (index == 0) ? F(WIFI1_SSID) : (index == 1) ? F(WIFI2_SSID) : @@ -40,7 +125,7 @@ const __FlashStringHelper* _wifiSSID(unsigned char index) { ); } -const __FlashStringHelper* _wifiPass(unsigned char index) { +const __FlashStringHelper* passphrase(size_t index) { return ( (index == 0) ? F(WIFI1_PASS) : (index == 1) ? F(WIFI2_PASS) : @@ -50,7 +135,7 @@ const __FlashStringHelper* _wifiPass(unsigned char index) { ); } -const __FlashStringHelper* _wifiIP(unsigned char index) { +const __FlashStringHelper* ip(size_t index) { return ( (index == 0) ? F(WIFI1_IP) : (index == 1) ? F(WIFI2_IP) : @@ -60,7 +145,7 @@ const __FlashStringHelper* _wifiIP(unsigned char index) { ); } -const __FlashStringHelper* _wifiGateway(unsigned char index) { +const __FlashStringHelper* gateway(size_t index) { return ( (index == 0) ? F(WIFI1_GW) : (index == 1) ? F(WIFI2_GW) : @@ -70,7 +155,7 @@ const __FlashStringHelper* _wifiGateway(unsigned char index) { ); } -const __FlashStringHelper* _wifiNetmask(unsigned char index) { +const __FlashStringHelper* mask(size_t index) { return ( (index == 0) ? F(WIFI1_MASK) : (index == 1) ? F(WIFI2_MASK) : @@ -80,7 +165,7 @@ const __FlashStringHelper* _wifiNetmask(unsigned char index) { ); } -const __FlashStringHelper* _wifiDNS(unsigned char index) { +const __FlashStringHelper* dns(size_t index) { return ( (index == 0) ? F(WIFI1_DNS) : (index == 1) ? F(WIFI2_DNS) : @@ -89,3 +174,6 @@ const __FlashStringHelper* _wifiDNS(unsigned char index) { (index == 4) ? F(WIFI5_DNS) : nullptr ); } + +} // namespace build +} // namespace wifi diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index 670b9522..dc7a5e35 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -13,9 +13,10 @@ Copyright (C) 2016-2019 by Xose Pérez #include #include "system.h" -#include "web.h" #include "ntp.h" #include "utils.h" +#include "web.h" +#include "wifi.h" #include "ws_internal.h" #include "libs/WebSocketIncommingBuffer.h" @@ -243,9 +244,12 @@ void WsDebug::send(bool connected) { } bool wsDebugSend(const char* prefix, const char* message) { - if (!wsConnected()) return false; - _ws_debug.add(prefix, message); - return true; + if (wifiConnected() && wsConnected()) { + _ws_debug.add(prefix, message); + return true; + } + + return false; } #endif @@ -480,8 +484,8 @@ void _wsOnConnected(JsonObject& root) { root["channel"] = WiFi.channel(); root["hostname"] = getSetting("hostname"); root["desc"] = getSetting("desc"); - root["network"] = getNetwork(); - root["deviceip"] = getIP(); + root["network"] = wifiStaSsid(); + root["deviceip"] = wifiStaIp().toString(); root["sketch_size"] = ESP.getSketchSize(); root["free_size"] = ESP.getFreeSketchSpace(); root["sdk"] = ESP.getSdkVersion(); @@ -530,7 +534,6 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); _wsConnected(client->id()); _wsResetUpdateTimer(); - wifiReconnectCheck(); client->_tempObject = new WebSocketIncommingBuffer(_wsParse, true); } else if(type == WS_EVT_DISCONNECT) { @@ -538,7 +541,7 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy if (client->_tempObject) { delete (WebSocketIncommingBuffer *) client->_tempObject; } - wifiReconnectCheck(); + wifiApCheck(); } else if(type == WS_EVT_ERROR) { DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u error(%u): %s\n"), client->id(), *((uint16_t*)arg), (char*)data); diff --git a/code/platformio.ini b/code/platformio.ini index 95242229..b6ce2c8f 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -136,7 +136,6 @@ lib_deps = https://github.com/vintlabs/fauxmoESP#3.1.2 https://github.com/xoseperez/hlw8012.git#1.1.0 IRremoteESP8266@2.7.4 - https://github.com/mcspr/justwifi.git#2cb9e769 https://github.com/xoseperez/my92xx#3.0.1 https://github.com/256dpi/arduino-mqtt#196556b6 https://bitbucket.org/xoseperez/nofuss.git#0.3.0 @@ -255,36 +254,6 @@ src_build_flags = -DESPURNA_CORE extends = env:esp8266-4m-base src_build_flags = -DESPURNA_CORE -[env:espurna-core-smartconfig-1MB] -extends = env:esp8266-1m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_SMARTCONFIG=1 - -[env:espurna-core-smartconfig-2MB] -extends = env:esp8266-2m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_SMARTCONFIG=1 - -[env:espurna-core-smartconfig-4MB] -extends = env:esp8266-4m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_SMARTCONFIG=1 - -[env:espurna-core-wps-1MB] -extends = env:esp8266-1m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_WPS=1 - -[env:espurna-core-wps-2MB] -extends = env:esp8266-2m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_WPS=1 - -[env:espurna-core-wps-4MB] -extends = env:esp8266-4m-base -src_build_flags = -DESPURNA_CORE -build_flags = ${common.build_flags} -DJUSTWIFI_ENABLE_WPS=1 - # ------------------------------------------------------------------------------ # ESPURNA CORE with WebUI # ------------------------------------------------------------------------------