ESPSomfy-RTS/src/ESPNetwork.cpp
cjkas 2517f40709 feat: settings ops, the WiFi and frequency scans, and reboot for socket protocol 2
Step 1d of the protocol 2 core (docs/SECURITY_PLAN.md).

- getSettings/saveSettings by section: general, network, mqtt, security,
  radio. A save is the settings route's request, so loop() applies it with
  the settings' own fromJSON(). ProtoSettings checks every field first:
  kind, range, string length, IP addresses.
- Secrets are only reported as has*. A security save replies with the
  client's new token and closes the other sockets; other saves send
  settingsChanged to the other clients.
- scanWifi runs an async scan from ESPNetwork, kept apart from the roaming
  scan, and wifiNetworks goes to the clients that asked.
- beginFrequencyScan, endFrequencyScan and reboot run at once on async_tcp,
  as their routes do: a scan's end mustn't wait in the loop queue behind an
  unpair that waits for the scan to end (new op kind 'a').
- e2e: test_settings.py and a --reboot flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:52:09 +01:00

730 lines
29 KiB
C++

#include <WiFi.h>
#include <esp_wifi.h>
#include <ESPmDNS.h>
#include "TaskWdt.h"
#include "esp_log.h"
#include "ConfigSettings.h"
#include "ESPNetwork.h"
#include "entryPoints/Web.h"
#include "outputs/SocketEmitter.h"
#include "outputs/ProtoSocket.h"
#include "Utils.h"
#include "SSDP.h"
#include "MqttConnection.h"
static const char *TAG = "Network";
extern ConfigSettings settings;
extern Web webServer;
extern SocketEmitter sockEmit;
extern MqttConnection mqtt;
extern rebootDelay_t rebootDelay;
extern ESPNetwork net;
extern SomfyShadeController somfy;
extern LoopStats loopStats;
static bool _apScanning = false;
// The scan for clients: asked for, then running. WiFi holds the results of
// one scan at a time, so it and the roaming scan never overlap.
static bool _clientScanWanted = false;
static bool _clientScanning = false;
int connectRetries = 0;
// A client that asks during a scan gets that scan's networks.
void ESPNetwork::scanForClients() { if(!_clientScanning) _clientScanWanted = true; }
static void clientScanLoop(ESPNetwork &net) {
if(_clientScanning) {
const int16_t n = WiFi.scanComplete();
if(n == WIFI_SCAN_RUNNING) return;
_clientScanning = false;
if(n < 0) ESP_LOGW(TAG, "WiFi scan failed");
else ESP_LOGI(TAG, "Scanned %d networks", n);
protoSocket.wifiNetworks(n);
WiFi.scanDelete();
return;
}
if(!_clientScanWanted || _apScanning) return;
_clientScanWanted = false;
// As /scanaps does: the soft AP's connection attempts would get in its way.
if(net.softAPOpened) WiFi.disconnect(false);
if(WiFi.scanNetworks(true, true) == WIFI_SCAN_RUNNING) _clientScanning = true;
else {
ESP_LOGW(TAG, "WiFi scan didn't start");
protoSocket.wifiNetworks(WIFI_SCAN_FAILED);
}
}
void ESPNetwork::end() {
SSDP.end();
mqtt.end();
sockEmit.end();
delay(100);
}
bool ESPNetwork::setup() {
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
WiFi.onEvent(this->networkEvent);
this->disconnectTime = millis();
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true, true);
if(settings.connType == conn_types_t::wifi || settings.connType == conn_types_t::unset) {
WiFi.persistent(false);
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
ESP_LOGI(TAG, "WiFi Mode: %d", WiFi.getMode());
WiFi.mode(WIFI_STA);
}
sockEmit.begin();
return true;
}
conn_types_t ESPNetwork::preferredConnType() {
switch(settings.connType) {
case conn_types_t::wifi:
return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap;
case conn_types_t::unset:
case conn_types_t::ap:
return conn_types_t::ap;
#if HAS_ETHERNET
case conn_types_t::ethernetpref:
return settings.WIFI.ssid[0] != '\0' && (!ETH.linkUp() && this->ethStarted) ? conn_types_t::wifi : conn_types_t::ethernet;
case conn_types_t::ethernet:
return ETH.linkUp() || !this->ethStarted ? conn_types_t::ethernet : conn_types_t::ap;
#else
// Stored by a backup or an older UI on a chip without Ethernet.
case conn_types_t::ethernetpref:
case conn_types_t::ethernet:
return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap;
#endif
default:
return settings.connType;
}
}
void ESPNetwork::loop() {
// ORDER OF OPERATIONS:
// ----------------------------------------------
// 1. If we are in the middle of a connection process we need to simply bail after the connect method. The
// connect method will take care of our target connection for us.
// 2. Check to see what type of target connection we need.
// a. If this is an ethernet target then the connection needs to perform a fallback if applicable.
// b. If this is a wifi target then we need to first check to see if the SSID is available.
// c. If an SSID has not been set then we need to turn on the Soft AP.
// 3. If the Soft AP is open and the target is either wifi, ethernet, or ethernetpref then
// we need to shut it down if there are no connections and the preferred connection is available.
// a. Ethernet: Check for an active ethernet connection. We cannot rely on linkup because the PHY will
// report that the link is up when no IP address is being served.
// b. WiFi: Perform synchronous scan for APs related to the SSID. If the SSID can be found then perform
// the connection process for the WiFi connection.
// c. SoftAP: This condition retains the Soft AP because no other connection method is available.
// Before the connection steps, which return early while connecting.
clientScanLoop(*this);
conn_types_t ctype = this->preferredConnType();
this->connect(ctype); // Connection timeout handled in connect function as well as the opening of the Soft AP if needed.
if(this->connecting()) return; // If we are currently attempting to connect to something then we need to bail here.
if(_apScanning) {
if(settings.WIFI.hidden || // This user has elected to use a hidden AP.
(this->connected() && !settings.WIFI.roaming) || // We are already connected and should not be roaming.
(this->softAPOpened && WiFi.softAPgetStationNum() != 0) || // The Soft AP is open and a user is connected.
(ctype != conn_types_t::wifi)) { // The Ethernet link is up so we should ignore this scan.
ESP_LOGI(TAG, "Cancelling WiFi STA Scan...");
_apScanning = false;
WiFi.scanDelete();
}
else {
int16_t n = WiFi.scanComplete();
if( n >= 0) { // If the scan is complete but the WiFi isn't ready this can return 0.
uint8_t bssid[6];
int32_t channel = 0;
if(this->getStrongestAP(settings.WIFI.ssid, bssid, &channel)) {
if(!WiFi.BSSID() || memcmp(bssid, WiFi.BSSID(), sizeof(bssid)) != 0) {
if(!this->connected()) {
ESP_LOGI(TAG, "Connecting to AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
this->connectWiFi(bssid, channel);
}
else {
ESP_LOGI(TAG, "Found stronger AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
this->changeAP(bssid, channel);
}
}
}
_apScanning = false;
}
}
}
if(!this->connecting() && !settings.WIFI.hidden && !_clientScanWanted && !_clientScanning) {
if((this->softAPOpened && WiFi.softAPgetStationNum() == 0) ||
(!this->connected() && ctype == conn_types_t::wifi)) {
// If the Soft AP is opened and there are no clients connected then we need to scan for an AP. If
// our target exists we will exit out of the Soft AP and start that connection. We are also
// going to continuously scan when there is no connection and our preferred connection is wifi.
if(ctype == conn_types_t::wifi) {
// Scan for an AP but only if we are not already scanning.
if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) {
_apScanning = true;
}
}
}
else if(this->connected() && ctype == conn_types_t::wifi && settings.WIFI.roaming) {
// Periodically look for a roaming AP.
if(millis() > SSID_SCAN_INTERVAL + this->lastWifiScan) {
//Serial.println("Started scan for access points");
if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) {
_apScanning = true;
this->lastWifiScan = millis();
}
}
}
}
if(millis() - this->lastEmit >= STATUS_EMIT_MS) {
this->lastEmit = millis();
if(this->connected()) this->emitSockets();
resetTaskWdt(); // Make sure we do not reboot here.
}
sockEmit.loop();
mqtt.loop();
if(settings.ssdpBroadcast && this->connected()) {
if(!SSDP.isStarted) SSDP.begin();
if(SSDP.isStarted) SSDP.loop();
}
else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end();
}
bool ESPNetwork::changeAP(const uint8_t *bssid, const int32_t channel) {
resetTaskWdt(); // Make sure we do not reboot here.
if(SSDP.isStarted) SSDP.end();
mqtt.disconnect();
//sockEmit.end();
WiFi.disconnect(false, true);
this->connType = conn_types_t::unset;
this->_connecting = true;
this->connectStart = millis();
WiFi.setScanMethod(WIFI_FAST_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
return false;
}
// The status broadcast, every STATUS_EMIT_MS from loop(). A wired link goes
// out every time, the WiFi signal only when it really changed.
void ESPNetwork::emitSockets() {
this->emitHeap();
if(this->connType != conn_types_t::wifi ||
this->rssiBroadcast.due(WiFi.RSSI(), WiFi.channel(), RSSI_EMIT_MIN_DBM))
this->emitSockets(255);
}
void ESPNetwork::emitSockets(uint8_t num) {
#if HAS_ETHERNET
if(this->connType == conn_types_t::ethernet) {
JsonSockEvent *json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", this->connected());
json->addElem("speed", (uint32_t)ETH.linkSpeed());
json->addElem("fullduplex", ETH.fullDuplex());
json->endObject();
sockEmit.endEmit(num);
}
else
#endif
{
if(WiFi.status() == WL_CONNECTED) {
JsonSockEvent *json = sockEmit.beginEmit("wifiStrength");
json->beginObject();
json->addElem("ssid", WiFi.SSID().c_str());
json->addElem("strength", (int32_t)WiFi.RSSI());
json->addElem("channel", (int32_t)this->channel);
json->endObject();
sockEmit.endEmit(num);
this->rssiBroadcast.sent(num == 255, WiFi.RSSI(), WiFi.channel());
}
else {
JsonSockEvent *json = sockEmit.beginEmit("wifiStrength");
json->beginObject();
json->addElem("ssid", "");
json->addElem("strength", (int8_t)-100);
json->addElem("channel", (int8_t)-1);
json->endObject();
sockEmit.endEmit(num);
json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", false);
json->addElem("speed", (uint8_t)0);
json->addElem("fullduplex", false);
json->endObject();
sockEmit.endEmit(num);
this->rssiBroadcast.sent(num == 255, -100, -1);
}
}
// A broadcast comes from emitSockets(), which has already sent memStatus.
if(num != 255) this->emitHeap(num);
// Protocol 2 clients get the status in their sync, so only broadcasts.
else if(this->connType == conn_types_t::ethernet) protoSocket.ethernet();
else protoSocket.wifi();
}
// Runs on the WiFi/Ethernet event task, so it sends nothing on the sockets.
// loop() broadcasts the new status and a client that reconnects gets it from
// SocketEmitter::initClients().
void ESPNetwork::setConnected(conn_types_t connType) {
resetTaskWdt();
this->connType = connType;
this->connectTime = millis();
connectRetries = 0;
ESP_LOGI(TAG, "Setting connected...");
if(this->connType == conn_types_t::wifi) {
if(this->softAPOpened && WiFi.softAPgetStationNum() == 0) {
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
}
this->_connecting = false;
this->ssid = WiFi.SSID();
this->mac = WiFi.BSSIDstr();
this->strength = WiFi.RSSI();
this->channel = WiFi.channel();
this->connectAttempts++;
}
#if HAS_ETHERNET
else if(this->connType == conn_types_t::ethernet) {
if(this->softAPOpened) {
ESP_LOGI(TAG, "Disconnecting from SoftAP");
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
}
this->connectAttempts++;
this->_connecting = false;
this->wifiFallback = false;
}
#endif // HAS_ETHERNET
// NET: Begin this in the startup.
//sockEmit.begin();
resetTaskWdt();
if(this->connectAttempts == 1) {
if(this->connType == conn_types_t::wifi) {
ESP_LOGI(TAG, "Successfully Connected to WiFi!!!! %s (%ddbm)", WiFi.localIP().toString().c_str(), this->strength);
if(settings.IP.dhcp) {
settings.IP.ip = WiFi.localIP();
settings.IP.subnet = WiFi.subnetMask();
settings.IP.gateway = WiFi.gatewayIP();
settings.IP.dns1 = WiFi.dnsIP(0);
settings.IP.dns2 = WiFi.dnsIP(1);
}
}
#if HAS_ETHERNET
else {
ESP_LOGI(TAG, "Successfully Connected to Ethernet!!! %s%s %dMbps", ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed());
if(settings.IP.dhcp) {
settings.IP.ip = ETH.localIP();
settings.IP.subnet = ETH.subnetMask();
settings.IP.gateway = ETH.gatewayIP();
settings.IP.dns1 = ETH.dnsIP(0);
settings.IP.dns2 = ETH.dnsIP(1);
}
}
#endif
}
else {
if(this->connType == conn_types_t::wifi) {
ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s %s CH:%d (%d dBm) Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, WiFi.localIP().toString().c_str(), this->mac.c_str(), this->channel, this->strength, this->connectAttempts - 1);
}
#if HAS_ETHERNET
else {
ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s%s %dMbps Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed(), this->connectAttempts - 1);
}
#endif
}
SSDP.setHTTPPort(80);
SSDP.setSchemaURL(0, "upnp.xml");
SSDP.setChipId(0, this->getChipId());
SSDP.setDeviceType(0, "urn:schemas-rstrouse-org:device:ESPSomfyRTS:1");
SSDP.setName(0, settings.hostname);
//SSDP.setSerialNumber(0, "C2496952-5610-47E6-A968-2FC19737A0DB");
//SSDP.setUUID(0, settings.uuid);
SSDP.setModelName(0, "ESPSomfy RTS");
if(strlen(settings.chipModel) == 0) SSDP.setModelNumber(0, "ESP32");
else {
char sModel[20] = "";
snprintf(sModel, sizeof(sModel), "ESP32-%s", settings.chipModel);
SSDP.setModelNumber(0, sModel);
}
SSDP.setModelURL(0, "https://github.com/rstrouse/ESPSomfy-RTS");
SSDP.setManufacturer(0, "rstrouse");
SSDP.setManufacturerURL(0, "https://github.com/rstrouse");
SSDP.setURL(0, "/");
SSDP.setActive(0, true);
resetTaskWdt();
if(MDNS.begin(settings.hostname)) {
ESP_LOGI(TAG, "MDNS Responder Started: serverId=%s", settings.serverId);
MDNS.addService("http", "tcp", 80);
//MDNS.addServiceTxt("http", "tcp", "board", "ESP32");
//MDNS.addServiceTxt("http", "tcp", "model", "ESPSomfyRTS");
MDNS.addService("espsomfy_rts", "tcp", 8080);
MDNS.addServiceTxt("espsomfy_rts", "tcp", "serverId", String(settings.serverId));
MDNS.addServiceTxt("espsomfy_rts", "tcp", "model", "ESPSomfyRTS");
MDNS.addServiceTxt("espsomfy_rts", "tcp", "version", String(settings.fwVersion.name));
}
if(settings.ssdpBroadcast) {
resetTaskWdt();
SSDP.begin();
}
else if(SSDP.isStarted) SSDP.end();
resetTaskWdt();
settings.printAvailHeap();
}
#if HAS_ETHERNET
bool ESPNetwork::connectWired() {
if(ETH.linkUp()) {
// If the ethernet link is re-established then we need to shut down wifi.
if(WiFi.status() == WL_CONNECTED) {
//sockEmit.end();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
if(this->connType != conn_types_t::ethernet) this->setConnected(conn_types_t::ethernet);
return true;
}
else if(this->ethStarted) {
// There is no wired connection so we need to fallback if appropriate.
if(settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0')
return this->connectWiFi();
}
if(this->connectAttempts > 0) {
ESP_LOGW(TAG, "Ethernet Connection Lost... %d Reconnecting %s", this->connectAttempts, this->mac.c_str());
}
else
ESP_LOGI(TAG, "Connecting to Wired Ethernet");
this->_connecting = true;
this->connTarget = conn_types_t::ethernet;
this->connType = conn_types_t::unset;
if(!this->ethStarted) {
// Currently the ethernet module will leak memory if you call begin more than once.
this->ethStarted = true;
WiFi.mode(WIFI_OFF);
if(settings.hostname[0] != '\0')
ETH.setHostname(settings.hostname);
else
ETH.setHostname("ESPSomfy-RTS");
ESP_LOGD(TAG, "Set hostname to: %s", ETH.getHostname());
eth_phy_type_t phyType;
bool begun = false;
if(!ethPhyType(settings.Ethernet.phyType, phyType))
ESP_LOGE(TAG, "Ethernet PHY type %u is not supported", (unsigned int)settings.Ethernet.phyType);
else
begun = ETH.begin(phyType, settings.Ethernet.phyAddress, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.PWRPin, settings.Ethernet.CLKMode);
if(!begun) {
ESP_LOGE(TAG, "Ethernet Begin failed");
this->ethStarted = false;
if(settings.connType == conn_types_t::ethernetpref) {
this->wifiFallback = true;
return connectWiFi();
}
return false;
}
else {
if(!settings.IP.dhcp) {
if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) {
ESP_LOGE(TAG, "Unable to configure static IP address....");
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
}
else
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
}
this->connectStart = millis();
return true;
}
#endif // HAS_ETHERNET
void ESPNetwork::updateHostname() {
if(settings.hostname[0] != '\0' && this->connected()) {
#if HAS_ETHERNET
if(this->connType == conn_types_t::ethernet &&
strcmp(settings.hostname, ETH.getHostname()) != 0) {
ESP_LOGD(TAG, "Updating host name to %s...", settings.hostname);
ETH.setHostname(settings.hostname);
MDNS.setInstanceName(settings.hostname);
SSDP.setName(0, settings.hostname);
}
else
#endif
if(strcmp(settings.hostname, WiFi.getHostname()) != 0) {
ESP_LOGD(TAG, "Updating host name to %s...", settings.hostname);
WiFi.setHostname(settings.hostname);
MDNS.setInstanceName(settings.hostname);
SSDP.setName(0, settings.hostname);
}
}
}
bool ESPNetwork::connectWiFi(const uint8_t *bssid, const int32_t channel) {
if(this->softAPOpened && WiFi.softAPgetStationNum() > 0) {
// There is a client connected to the soft AP. We do not want to close out the connection. While both the
// Soft AP and a wifi connection can coexist on ESP32 the performance is abysmal.
WiFi.disconnect(false);
this->_connecting = false;
this->connType = conn_types_t::unset;
return true;
}
WiFi.setSleep(false);
if(!settings.IP.dhcp) {
if(!WiFi.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2))
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
else
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
delay(100);
if(bssid && channel > 0) {
if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0
&& WiFi.channel() == channel) {
this->disconnected = 0;
return true;
}
this->connTarget = conn_types_t::wifi;
this->connType = conn_types_t::unset;
ESP_LOGI(TAG, "WiFi begin...");
this->_connecting = true;
// Our own scan already picked the AP. An all-channel scan here makes the
// driver sweep every channel again first, about 17s, before it even
// authenticates. A fast scan starts on the AP's channel and joins the
// first match.
WiFi.setScanMethod(WIFI_FAST_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
}
else if(settings.WIFI.ssid[0] != '\0') {
if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0) {
// If we are connected to the target SSID then just return.
this->disconnected = 0;
this->_connecting = true;
return true;
}
if(this->_connecting) return true;
this->_connecting = true;
this->connTarget = conn_types_t::wifi;
this->connType = conn_types_t::unset;
if(this->connectAttempts > 0) {
ESP_LOGW(TAG, "Connection Lost... %s CH:%d (%ddbm)", this->mac.c_str(), this->channel, this->strength);
}
else ESP_LOGI(TAG, "Connecting to AP");
delay(100);
// There is also another method simply called hostname() but this is legacy for esp8266.
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
ESP_LOGD(TAG, "Set hostname to: %s", WiFi.getHostname());
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
uint8_t _bssid[6];
int32_t _channel = 0;
if(!settings.WIFI.hidden && this->getStrongestAP(settings.WIFI.ssid, _bssid, &_channel)) {
ESP_LOGD(TAG, "Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel);
WiFi.setScanMethod(WIFI_FAST_SCAN); // The AP is known, see connectWiFi(bssid).
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, _channel, _bssid);
}
else {
// If the user has the hidden flag set just connect to whatever the AP gives us.
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase);
}
}
this->connectStart = millis();
return true;
}
bool ESPNetwork::connect(conn_types_t ctype) {
resetTaskWdt();
if(this->connecting()) return true;
if(this->disconnectTime == 0) this->disconnectTime = millis();
#if HAS_ETHERNET
if(ctype == conn_types_t::ethernet && this->connType != conn_types_t::ethernet) {
// Here we need to call the connect to ethernet.
this->connectWired();
}
else
#endif
if(ctype == conn_types_t::ap || (!this->connected() && millis() > this->disconnectTime + CONNECT_TIMEOUT)) {
if(!this->softAPOpened && !this->openingSoftAP) {
this->disconnectTime = millis();
this->openSoftAP();
}
else if(this->softAPOpened && !this->openingSoftAP &&
(ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) {
// When thge softAP is open then we need to try to connect to wifi repeatedly if the user connects to a hidden SSID.
this->connectWiFi();
}
}
else if((ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) {
this->connectWiFi();
}
return true;
}
uint32_t ESPNetwork::getChipId() {
uint32_t chipId = 0;
uint64_t mac = ESP.getEfuseMac();
for(int i=0; i<17; i=i+8) {
chipId |= ((mac >> (40 - i)) & 0xff) << i;
}
return chipId;
}
bool ESPNetwork::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) {
const bool connected = this->connected();
const int32_t currentRssi = connected ? WiFi.RSSI() : 0;
int32_t strength = -127;
int32_t chan = -1;
memset(bssid, 0x00, 6);
const bool wdtPaused = pauseTaskWdt();
int16_t n = WiFi.scanComplete();
//int16_t n = this->connected() ? WiFi.scanComplete() : WiFi.scanNetworks(false, false, false, 300, 0, ssid);
resumeTaskWdt(wdtPaused);
for(int16_t i = 0; i < n; i++) {
if(WiFi.SSID(i).compareTo(ssid) == 0) {
// Only on the roaming scans, so the log shows whether a better AP is
// around without filling up while we scan to reconnect.
if(connected) {
const uint8_t *b = WiFi.BSSID(i);
const uint8_t *cur = WiFi.BSSID();
ESP_LOGI(TAG, "AP %02X:%02X:%02X:%02X:%02X:%02X ch %ld %ld dBm %s%s", b[0], b[1], b[2], b[3], b[4], b[5],
(long)WiFi.channel(i), (long)WiFi.RSSI(i), wifiAuthModeName(WiFi.encryptionType(i)),
(cur && memcmp(b, cur, 6) == 0) ? " (connected)" : "");
}
if(worthRoaming(connected, currentRssi, WiFi.RSSI(i), ROAM_MIN_GAIN_DB) && WiFi.RSSI(i) > strength) {
strength = WiFi.RSSI(i);
memcpy(bssid, WiFi.BSSID(i), 6);
*channel = chan = WiFi.channel(i);
}
}
}
WiFi.scanDelete();
return chan > 0;
}
bool ESPNetwork::openSoftAP() {
if(this->softAPOpened || this->openingSoftAP) return true;
if(this->connected()) WiFi.disconnect(false);
this->openingSoftAP = true;
ESP_LOGI(TAG, "Turning the HotSpot On");
resetTaskWdt(); // Make sure we do not reboot here.
WiFi.softAP(strlen(settings.hostname) > 0 ? settings.hostname : "ESPSomfy RTS", "");
delay(200);
return true;
}
bool ESPNetwork::connected() {
if(this->connecting()) return false;
else if(this->connType == conn_types_t::unset) return false;
else if(this->connType == conn_types_t::wifi) return WiFi.status() == WL_CONNECTED;
#if HAS_ETHERNET
else if(this->connType == conn_types_t::ethernet) return ETH.linkUp();
#endif
else return this->connType != conn_types_t::unset;
return false;
}
bool ESPNetwork::connecting() {
if(this->_connecting && millis() > this->connectStart + CONNECT_TIMEOUT) this->_connecting = false;
return this->_connecting;
}
void ESPNetwork::clearConnecting() { this->_connecting = false; }
void ESPNetwork::networkEvent(WiFiEvent_t event) {
switch(event) {
case ARDUINO_EVENT_WIFI_READY: ESP_LOGI(TAG, "(evt) WiFi interface ready"); break;
case ARDUINO_EVENT_WIFI_SCAN_DONE:
ESP_LOGI(TAG, "(evt) Completed scan for access points (%d)", WiFi.scanComplete());
//Serial.println("(evt) Completed scan for access points");
net.lastWifiScan = millis();
break;
case ARDUINO_EVENT_WIFI_STA_START:
ESP_LOGI(TAG, "WiFi station mode started");
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
break;
case ARDUINO_EVENT_WIFI_STA_STOP: ESP_LOGI(TAG, "(evt) WiFi clients stopped"); break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED: {
// How long the join took and the security the AP offers, so a slow
// connect shows in the log.
wifi_ap_record_t ap;
const bool haveAp = esp_wifi_sta_get_ap_info(&ap) == ESP_OK;
ESP_LOGI(TAG, "(evt) Connected to WiFi STA access point after %lums (%s)",
(unsigned long)(millis() - net.connectStart), haveAp ? wifiAuthModeName(ap.authmode) : "?");
break;
}
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
ESP_LOGI(TAG, "(evt) Disconnected from WiFi STA access point. Connecting: %d", net.connecting());
net.connType = conn_types_t::unset;
net.disconnectTime = millis();
net.clearConnecting();
break;
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: ESP_LOGI(TAG, "(evt) Authentication mode of STA access point has changed"); break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
ESP_LOGI(TAG, "(evt) Got WiFi STA IP: %s", WiFi.localIP().toString().c_str());
net.connType = conn_types_t::wifi;
net.connectTime = millis();
net.setConnected(conn_types_t::wifi);
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP: ESP_LOGW(TAG, "Lost IP address and IP address is reset to 0"); break;
#if HAS_ETHERNET
case ARDUINO_EVENT_ETH_GOT_IP:
// If the Wifi is connected then drop that connection
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true);
ESP_LOGI(TAG, "Got Ethernet IP %s", ETH.localIP().toString().c_str());
net.connectTime = millis();
net.connType = conn_types_t::ethernet;
if(settings.IP.dhcp) {
settings.IP.ip = ETH.localIP();
settings.IP.subnet = ETH.subnetMask();
settings.IP.gateway = ETH.gatewayIP();
settings.IP.dns1 = ETH.dnsIP(0);
settings.IP.dns2 = ETH.dnsIP(1);
}
net.setConnected(conn_types_t::ethernet);
break;
case ARDUINO_EVENT_ETH_CONNECTED:
ESP_LOGI(TAG, "(evt) Ethernet Connected");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
ESP_LOGI(TAG, "(evt) Ethernet Disconnected");
net.connType = conn_types_t::unset;
net.disconnectTime = millis();
net.clearConnecting();
break;
case ARDUINO_EVENT_ETH_START:
ESP_LOGI(TAG, "(evt) Ethernet Started");
net.ethStarted = true;
break;
case ARDUINO_EVENT_ETH_STOP:
ESP_LOGI(TAG, "(evt) Ethernet Stopped");
net.connType = conn_types_t::unset;
net.ethStarted = false;
break;
#endif
case ARDUINO_EVENT_WIFI_AP_START:
ESP_LOGI(TAG, "(evt) WiFi SoftAP Started IP: %s", WiFi.softAPIP().toString().c_str());
net.openingSoftAP = false;
net.softAPOpened = true;
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
if(!net.openingSoftAP) ESP_LOGI(TAG, "(evt) WiFi SoftAP Stopped");
net.softAPOpened = false;
break;
default:
#if HAS_ETHERNET
if(event > ARDUINO_EVENT_ETH_START)
ESP_LOGW(TAG, "(evt) Unknown Ethernet Event %d", event);
#endif
break;
}
}
void ESPNetwork::emitHeap(uint8_t num) {
uint32_t freeHeap = ESP.getFreeHeap();
uint32_t maxHeap = ESP.getMaxAllocHeap();
uint32_t minHeap = ESP.getMinFreeHeap();
JsonSockEvent *json = sockEmit.beginEmit("memStatus");
json->beginObject();
json->addElem("max", maxHeap);
json->addElem("free", freeHeap);
json->addElem("min", minHeap);
json->addElem("total", ESP.getHeapSize());
json->addElem("uptime", (uint64_t)millis());
// The window in progress. Only a broadcast closes it, so a client
// connecting doesn't wipe a slow pass before Home Assistant has seen it.
json->addElem("loopRate", loopStats.rate(millis()));
json->addElem("loopMaxUs", loopStats.maxUs());
json->endObject();
sockEmit.endEmit(num);
if(num == 255) {
protoSocket.memory();
loopStats.publish(millis());
}
}