/* 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" #include "wifi_config.h" #include "telnet.h" #include "ws.h" #include #if WIFI_AP_CAPTIVE_SUPPORT #include #endif #include #include #include #include // ----------------------------------------------------------------------------- // SETTINGS // ----------------------------------------------------------------------------- namespace settings { namespace internal { 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 wifi::ApMode::Disabled; case 1: return wifi::ApMode::Enabled; case 2: return wifi::ApMode::Fallback; } return wifi::build::softApMode(); } template <> WiFiSleepType_t convert(const String& value) { switch (value.toInt()) { case 2: return WIFI_MODEM_SLEEP; case 1: return WIFI_LIGHT_SLEEP; case 0: return WIFI_NONE_SLEEP; } return wifi::build::sleep(); } template <> IPAddress convert(const String& value) { IPAddress out; out.fromString(value); return out; } // XXX: "(IP unset)" when not set, no point saving these :/ String serialize(const IPAddress& ip) { return ip.isSet() ? ip.toString() : emptyString; } } // namespace internal } // namespace settings // ----------------------------------------------------------------------------- // INTERNAL // ----------------------------------------------------------------------------- namespace wifi { // 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. constexpr uint8_t OpmodeNull { NULL_MODE }; constexpr uint8_t OpmodeSta { STATION_MODE }; constexpr uint8_t OpmodeAp { SOFTAP_MODE }; constexpr uint8_t OpmodeApSta { OpmodeSta | OpmodeAp }; using Mac = std::array; using Macs = std::vector; enum class ScanError { None, AlreadyScanning, System, NoNetworks }; enum class Action { StationConnect, StationContinueConnect, StationTryConnectBetter, StationDisconnect, AccessPointFallback, AccessPointFallbackCheck, AccessPointStart, AccessPointStop, TurnOff, TurnOn }; using Actions = std::list; using ActionsQueue = std::queue; enum class State { Boot, Connect, TryConnectBetter, Connected, Idle, Init, Timeout, Fallback, WaitScan, WaitScanWithoutCurrent, WaitConnected }; namespace internal { // 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 bool enabled { true }; ActionsQueue actions; } // namespace internal uint8_t opmode() { return wifi_get_opmode(); } bool enabled() { return internal::enabled; } void enable() { internal::enabled = true; } void disable() { internal::enabled = false; } 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; } internal::actions.push(value); } ActionsQueue& actions() { return internal::actions; } // ::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; } } return false; } bool wakeup() { if (wifi_fpm_get_sleep_type() != NONE_SLEEP_T) { wifi_fpm_do_wakeup(); wifi_fpm_close(); delay(10); return true; } 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; } return ptr; } String ip(const IPAddress& addr) { return addr.toString(); } String ip(ip4_addr_t addr) { String out; out.reserve(16); bool delim { false }; for (int byte = 0; byte < 4; ++byte) { if (delim) { out += '.'; } out += ip4_addr_get_byte_val(addr, byte); delim = true; } 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; } 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; } return ptr; } 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; } return ptr; } } // namespace debug namespace settings { void migrate(int version) { if (version && (version < 5)) { moveSetting("apmode", "wifiApMode"); } } decltype(millis()) garpInterval() { return getSetting("wifiGarpIntvl", secureRandom(wifi::build::garpIntervalMin(), wifi::build::garpIntervalMax())); } float txPower() { return getSetting("wifiTxPwr", wifi::build::outputDbm()); } WiFiSleepType_t sleep() { return getSetting("wifiSleep", wifi::build::sleep()); } bool scanNetworks() { return getSetting("wifiScan", wifi::build::scanNetworks()); } int8_t scanRssiThreshold() { return getSetting("wifiScanRssi", wifi::build::scanRssiThreshold()); } String hostname() { return getSetting("hostname", getIdentifier()); } 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))); } String staSsid(size_t index) { return getSetting({"ssid", index}, wifi::build::ssid(index)); } String staPassphrase(size_t index) { return getSetting({"pass", index}, wifi::build::passphrase(index)); } 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))); } IPAddress staDns(size_t index) { return ::settings::internal::convert( getSetting({"dns", index}, wifi::build::dns(index))); } bool softApCaptive() { return getSetting("wifiApCaptive", wifi::build::softApCaptive()); } wifi::ApMode softApMode() { return getSetting("wifiApMode", wifi::build::softApMode()); } String softApSsid() { return getSetting("wifiApSsid", wifi::build::hasSoftApSsid() ? wifi::build::softApSsid() : hostname()); } 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 }; auto value = getSetting({"wifiApLease", index}); if (12 == value.length()) { hexDecode(value.c_str(), value.length(), lease.data(), lease.size()); } return lease; } } // namespace settings // 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 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'; 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; } explicit operator bool() const { return _rssi != 0 && _channel != 0; } bool operator<(const Info& rhs) const { return _rssi < rhs._rssi; } bool operator>(const Info& rhs) const { return _rssi > rhs._rssi; } const wifi::Mac& bssid() const { return _bssid; } AUTH_MODE authmode() const { return _authmode; } int8_t rssi() const { return _rssi; } 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) {} SsidInfo(String&& ssid, wifi::Info&& info) : _ssid(std::move(ssid)), _info(std::move(info)) {} const String& ssid() const { return _ssid; } const wifi::Info& info() const { return _info; } // 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; }; using SsidInfos = std::forward_list; // 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? struct IpSettings { IpSettings() = default; IpSettings(const IpSettings&) = default; IpSettings(IpSettings&&) = default; IpSettings& operator=(const IpSettings&) = default; IpSettings& operator=(IpSettings&&) = default; 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)) {} const IPAddress& ip() const { return _ip; } const IPAddress& gateway() const { return _gateway; } const IPAddress& netmask() const { return _netmask; } const IPAddress& dns() const { return _dns; } explicit operator bool() const { return _ip.isSet() && _gateway.isSet() && _netmask.isSet() && _dns.isSet(); } private: IPAddress _ip; IPAddress _gateway; IPAddress _netmask; IPAddress _dns; }; struct StaNetwork { Mac bssid; String ssid; String passphrase; int8_t rssi; uint8_t channel; }; struct SoftApNetwork { Mac bssid; String ssid; String passphrase; uint8_t channel; AUTH_MODE authmode; }; 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; } const String& ssid() const { return _ssid; } const String& passphrase() const { return _passphrase; } const IpSettings& ipSettings() const { return _ipSettings; } const wifi::Mac& bssid() const { return _bssid; } uint8_t channel() const { return _channel; } private: String _ssid; String _passphrase; IpSettings _ipSettings; Mac _bssid {}; uint8_t _channel { 0u }; }; using Networks = std::list; // ----------------------------------------------------------------------------- // STATION // ----------------------------------------------------------------------------- namespace sta { 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(); } // 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()}; } wifi::Info info() { station_config config{}; wifi_station_get_config(&config); return info(config); } wifi::IpSettings ipsettings() { return { WiFi.localIP(), WiFi.gatewayIP(), WiFi.subnetMask(), WiFi.dnsIP()}; } wifi::Mac bssid() { station_config config{}; wifi_station_get_config(&config); return convertBssid(config); } wifi::StaNetwork current(const station_config& config) { return { convertBssid(config), convertSsid(config), convertPassphrase(config), rssi(), channel()}; } wifi::StaNetwork current() { station_config config{}; wifi_station_get_config(&config); return current(config); } #if WIFI_GRATUITOUS_ARP_SUPPORT namespace garp { namespace internal { Ticker timer; bool wait { false }; decltype(millis()) interval { wifi::build::garpIntervalMin() }; } // namespace internal bool send() { 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) && (!ip4_addr_isany_val(*netif_ip4_addr(interface))) && (interface->flags & NETIF_FLAG_LINK_UP) && (interface->flags & NETIF_FLAG_UP) ) { etharp_gratuitous(interface); result = true; } } return result; } bool wait() { if (internal::wait) { return true; } internal::wait = true; return false; } void stop() { internal::timer.detach(); } void start(decltype(millis()) ms) { internal::timer.attach_ms(ms, []() { internal::wait = false; }); } } // namespace garp #endif namespace scan { using SsidInfosPtr = std::shared_ptr; using Success = std::function; using Error = std::function; struct Task { Task() = delete; template Task(S&& success, E&& error) : _success(std::forward(success)), _error(std::forward(error)) {} void success(bss_info* info) { _success(info); } void error(wifi::ScanError error) { _error(error); } private: Success _success; Error _error; }; using TaskPtr = std::unique_ptr; namespace internal { TaskPtr task; void stop() { task = nullptr; } // 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; } 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); } if (!networks) { task->error(wifi::ScanError::NoNetworks); } stop(); } } // namespace internal bool start(Success&& success, Error&& error) { if (internal::task) { error(wifi::ScanError::AlreadyScanning); return false; } // 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) // 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 //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; } error(wifi::ScanError::System); return false; } // 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); } return result; } // 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(); }); return infos; } } // namespace scan bool enabled() { return wifi::opmode() & wifi::OpmodeSta; } // XXX: WiFi.disconnect() also implicitly disables STA mode *and* erases the current STA config void disconnect() { if (enabled()) { wifi_station_disconnect(); } } // 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 disable() { if (!WiFi.enableSTA(false)) { abort(); } } namespace connection { namespace internal { 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(); } size_t count() const { return _networks.size(); } bool done() const { return _current == _end; } 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; } // 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 WiFiEventStationModeDisconnected& src) { // aka const auto& 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(); } 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(); 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 } // 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; } 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"; } return ""; } } // namespace debug namespace internal { // 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); } namespace { } // namespace 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(); } if (wifi::sta::connecting()) { wifi::sta::connection::unlock(); wifi::sta::connection::stop(); } if (wifi::sta::enabled()) { wifi::sta::disable(); publish(wifi::Event::Mode); } break; 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 wifiStartAp() { wifi::action(wifi::Action::AccessPointStart); } 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); }