Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2437 lines
61 KiB

/*
WIFI MODULE
Original code based on JustWifi, Wifi Manager for ESP8266 (GPLv3+)
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Modified for ESPurna
Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#include "wifi.h"
#include "wifi_config.h"
#include "telnet.h"
#include "ws.h"
#include <AddrList.h>
#if WIFI_AP_CAPTIVE_SUPPORT
#include <DNSServer.h>
#endif
#include <algorithm>
#include <array>
#include <queue>
#include <vector>
// -----------------------------------------------------------------------------
// SETTINGS
// -----------------------------------------------------------------------------
namespace settings {
namespace internal {
template<>
wifi::StaMode convert(const String& value) {
return convert<bool>(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<uint8_t, 6>;
using Macs = std::vector<Mac>;
enum class ScanError {
None,
AlreadyScanning,
System,
NoNetworks
};
enum class Action {
StationConnect,
StationContinueConnect,
StationTryConnectBetter,
StationDisconnect,
AccessPointFallback,
AccessPointFallbackCheck,
AccessPointStart,
AccessPointStop,
TurnOff,
TurnOn
};
using Actions = std::list<Action>;
using ActionsQueue = std::queue<Action, Actions>;
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<IPAddress>(
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<IPAddress>(
getSetting({"gw", index}, wifi::build::gateway(index)));
}
IPAddress staMask(size_t index) {
return ::settings::internal::convert<IPAddress>(
getSetting({"mask", index}, wifi::build::mask(index)));
}
IPAddress staDns(size_t index) {
return ::settings::internal::convert<IPAddress>(
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<const char*>(config.ssid), config.ssid_len);
return ssid;
}
String convertSsid(const bss_info& info) {
String ssid;
ssid.concat(reinterpret_cast<const char*>(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<const char*>(config.ssid) };
char ssid[SsidSize + 1];
std::copy(ptr, ptr + SsidSize, ssid);
ssid[SsidSize] = '\0';
return ssid;
}
template <typename T, size_t PassphraseSize = sizeof(T::password)>
String convertPassphrase(const T& config) {
const char* ptr { reinterpret_cast<const char*>(config.password) };
char passphrase[PassphraseSize + 1];
std::copy(ptr, ptr + PassphraseSize, passphrase);
passphrase[PassphraseSize] = '\0';
return passphrase;
}
template <typename T>
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<SsidInfo>;
// 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 <typename Ip, typename Gateway, typename Netmask, typename Dns>
IpSettings(Ip&& ip, Gateway&& gateway, Netmask&& netmask, Dns&& dns) :
_ip(std::forward<Ip>(ip)),
_gateway(std::forward<Gateway>(gateway)),
_netmask(std::forward<Netmask>(netmask)),
_dns(std::forward<Dns>(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 <typename Ssid>
explicit Network(Ssid&& ssid) :
_ssid(std::forward<Ssid>(ssid))
{}
template <typename Ssid, typename Passphrase>
Network(Ssid&& ssid, Passphrase&& passphrase) :
_ssid(std::forward<Ssid>(ssid)),
_passphrase(std::forward<Passphrase>(passphrase))
{}
template <typename Ssid, typename Passphrase, typename Settings>
Network(Ssid&& ssid, Passphrase&& passphrase, Settings&& settings) :
_ssid(std::forward<Ssid>(ssid)),
_passphrase(std::forward<Passphrase>(passphrase)),
_ipSettings(std::forward<Settings>(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<Network>;
// -----------------------------------------------------------------------------
// 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<wifi::SsidInfos>;
using Success = std::function<void(bss_info*)>;
using Error = std::function<void(wifi::ScanError)>;
struct Task {
Task() = delete;
template <typename S, typename E>
Task(S&& success, E&& error) :
_success(std::forward<S>(success)),
_error(std::forward<E>(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<Task>;
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<bss_info*>(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<Task>(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<wifi::SsidInfos>();
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<Task>;
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<bool>(internal::task);
}
void start(String&& hostname, Networks&& networks, int retries) {
if (!locked()) {
internal::task = std::make_unique<internal::Task>(
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<bool>(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<bool>(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 <typename T>
void lease(T&& mac) {
if (internal::leases.size() < LeasesMax) {
internal::leases.push_back(std::forward<T>(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<wifi::EventCallback>;
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);
}