From 5a97329832816219a919c4669e22ad6af0c8d228 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Wed, 31 Mar 2021 03:04:53 +0300 Subject: [PATCH] wifi: refactoring / rewrite - replace JustWifi with a custom WiFi module - re-implemented Core methods that deal with scanning, directly work with the SDK callback - re-implemented Core method for opmode to avoid dealing with the 2.7.x pseudo modes - re-implemented Core method for forced-(modem)-sleep (with the intention to merge with OpmodeNull... todo) - disable reconnect and autoconnect from SDK, avoid unintentionally storing ssid & pass for these and make connection routine watch for events instead. timeout is implemented as a local timer - do not store runtime data that can be retrieved via SDK calls - settings are loaded only when starting AP or STA, networks are no longer kept in memory - settings STA ssidN must be unique - remove char buffer from the event callback - trigger station-disconnected event when actually disconnected, not when connecting or explicitly requesting it - generic rssi sorting method for initial connection, do rssi checks while connected to find a better network Drop (temporarily?) wps and smartconfig. Both are implementable with the new approach, but come with some weird expectations from the SDK side (no extra heap with wps, broadcasting plain-text passphrase with smartconfig, storing station config implicitly, etc.). Both are sort-of fixed with RTOS SDK, but it is still a big question of whether to support them at all in the current state. --- code/espurna/alexa.cpp | 5 +- code/espurna/button.cpp | 12 +- code/espurna/config/general.h | 69 +- code/espurna/config/types.h | 10 - code/espurna/led.cpp | 155 +- code/espurna/led.h | 6 +- code/espurna/mdns.cpp | 18 +- code/espurna/mqtt.cpp | 6 +- code/espurna/telnet.cpp | 7 +- code/espurna/tuya.cpp | 17 +- code/espurna/wifi.cpp | 2736 ++++++++++++++++++++++++++------- code/espurna/wifi.h | 64 +- code/espurna/wifi_config.h | 104 +- code/espurna/ws.cpp | 19 +- code/platformio.ini | 31 - 15 files changed, 2471 insertions(+), 788 deletions(-) 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 # ------------------------------------------------------------------------------