/*
|
|
|
|
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 auto& src) {
|
|
connection::internal::connected = false;
|
|
});
|
|
disconnect();
|
|
disable();
|
|
}
|
|
|
|
void toggle() {
|
|
auto current = enabled();
|
|
connection::persist(!current);
|
|
wifi::action(current
|
|
? wifi::Action::StationDisconnect
|
|
: wifi::Action::StationConnect);
|
|
}
|
|
|
|
namespace scan {
|
|
namespace periodic {
|
|
namespace internal {
|
|
|
|
constexpr int8_t Checks { wifi::build::scanRssiChecks() };
|
|
constexpr decltype(millis()) CheckInterval { wifi::build::scanRssiCheckInterval() };
|
|
|
|
int8_t threshold { wifi::build::scanRssiThreshold() };
|
|
int8_t counter { Checks };
|
|
Ticker timer;
|
|
|
|
void task() {
|
|
if (!wifi::sta::connected()) {
|
|
counter = Checks;
|
|
return;
|
|
}
|
|
|
|
auto rssi = wifi::sta::rssi();
|
|
if (rssi > threshold) {
|
|
counter = Checks;
|
|
} else if (rssi < threshold) {
|
|
if (counter < 0) {
|
|
return;
|
|
}
|
|
|
|
if (!--counter) {
|
|
wifi::action(wifi::Action::StationTryConnectBetter);
|
|
}
|
|
}
|
|
}
|
|
|
|
void start() {
|
|
counter = Checks;
|
|
timer.attach_ms(CheckInterval, task);
|
|
}
|
|
|
|
void stop() {
|
|
counter = Checks;
|
|
timer.detach();
|
|
}
|
|
|
|
} // namespace internal
|
|
|
|
void threshold(int8_t value) {
|
|
internal::threshold = value;
|
|
}
|
|
|
|
int8_t threshold() {
|
|
return internal::threshold;
|
|
}
|
|
|
|
void stop() {
|
|
internal::stop();
|
|
}
|
|
|
|
void start() {
|
|
internal::start();
|
|
}
|
|
|
|
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 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);
|
|
}
|