Merge pull request #3057 from proddy/core3

updates to Network code
This commit is contained in:
Proddy
2026-05-03 23:04:19 +02:00
committed by GitHub
21 changed files with 351 additions and 210 deletions

View File

@@ -1151,8 +1151,8 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.25: baseline-browser-mapping@2.10.27:
resolution: {integrity: sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==} resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
@@ -2993,9 +2993,9 @@ packages:
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
stack-trace@1.0.0-pre2: stack-trace@1.0.0:
resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} resolution: {integrity: sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==}
engines: {node: '>=16'} engines: {node: '>=20.0.0'}
strict-uri-encode@1.1.0: strict-uri-encode@1.1.0:
resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
@@ -4279,7 +4279,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
baseline-browser-mapping@2.10.25: {} baseline-browser-mapping@2.10.27: {}
bin-build@3.0.0: bin-build@3.0.0:
dependencies: dependencies:
@@ -4340,7 +4340,7 @@ snapshots:
browserslist@4.28.2: browserslist@4.28.2:
dependencies: dependencies:
baseline-browser-mapping: 2.10.25 baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001791 caniuse-lite: 1.0.30001791
electron-to-chromium: 1.5.349 electron-to-chromium: 1.5.349
node-releases: 2.0.38 node-releases: 2.0.38
@@ -6178,7 +6178,7 @@ snapshots:
stable@0.1.8: {} stable@0.1.8: {}
stack-trace@1.0.0-pre2: {} stack-trace@1.0.0: {}
strict-uri-encode@1.1.0: {} strict-uri-encode@1.1.0: {}
@@ -6420,7 +6420,7 @@ snapshots:
node-html-parser: 6.1.13 node-html-parser: 6.1.13
simple-code-frame: 1.3.0 simple-code-frame: 1.3.0
source-map: 0.7.6 source-map: 0.7.6
stack-trace: 1.0.0-pre2 stack-trace: 1.0.0
vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.2) vite: 8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.2)
vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.2): vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.2):

View File

@@ -173,7 +173,7 @@ const NetworkSettings = () => {
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors || {}}
name="ssid" name="ssid"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'} label="SSID"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={data.ssid} value={data.ssid}

View File

@@ -261,7 +261,6 @@ const cz: Translation = {
SCAN_AGAIN: 'Skenovat znovu', SCAN_AGAIN: 'Skenovat znovu',
NETWORK_SCANNER: 'Síťový skener', NETWORK_SCANNER: 'Síťový skener',
NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě', NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě',
NETWORK_BLANK_SSID: 'ponechte prázdné pro deaktivaci WiFi a povolení ETH',
NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID', NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID',
TX_POWER: 'Vysílací výkon', TX_POWER: 'Vysílací výkon',
HOSTNAME: 'Název hostitele', HOSTNAME: 'Název hostitele',

View File

@@ -261,7 +261,6 @@ const de: Translation = {
SCAN_AGAIN: 'Erneute Suche', SCAN_AGAIN: 'Erneute Suche',
NETWORK_SCANNER: 'Netzwerksuche', NETWORK_SCANNER: 'Netzwerksuche',
NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden', NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden',
NETWORK_BLANK_SSID: 'Freilassen, um WiFi zu deaktivieren und ETH zu aktivieren.',
NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.', NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.',
TX_POWER: 'Tx Leistung', TX_POWER: 'Tx Leistung',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',

View File

@@ -261,7 +261,6 @@ const en: Translation = {
SCAN_AGAIN: 'Scan again', SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner', NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found', NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi and enable ETH',
NETWORK_BLANK_BSSID: 'leave blank to use only SSID', NETWORK_BLANK_BSSID: 'leave blank to use only SSID',
TX_POWER: 'Tx Power', TX_POWER: 'Tx Power',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',

View File

@@ -261,7 +261,6 @@ const fr: Translation = {
SCAN_AGAIN: 'Rescanner', SCAN_AGAIN: 'Rescanner',
NETWORK_SCANNER: 'Scan réseau', NETWORK_SCANNER: 'Scan réseau',
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé', NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi',
NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID', NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID',
TX_POWER: 'Puissance Tx', TX_POWER: 'Puissance Tx',
HOSTNAME: "Nom d'hôte", HOSTNAME: "Nom d'hôte",

View File

@@ -261,7 +261,6 @@ const it: Translation = {
SCAN_AGAIN: 'Scansiona ancora', SCAN_AGAIN: 'Scansiona ancora',
NETWORK_SCANNER: 'Scansione Rete', NETWORK_SCANNER: 'Scansione Rete',
NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata', NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata',
NETWORK_BLANK_SSID: 'lasciare vuoto per disattivare WiFi',
NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID', NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID',
TX_POWER: 'Potenza Tx', TX_POWER: 'Potenza Tx',
HOSTNAME: 'Nome ospite', HOSTNAME: 'Nome ospite',

View File

@@ -261,7 +261,6 @@ const nl: Translation = {
SCAN_AGAIN: 'Opnieuw scannen', SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scannen', NETWORK_SCANNER: 'Netwerk Scannen',
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden', NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken', NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
TX_POWER: 'Tx Vermogen', TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostnaam', HOSTNAME: 'Hostnaam',

View File

@@ -261,7 +261,6 @@ const no: Translation = {
SCAN_AGAIN: 'Søk igjen', SCAN_AGAIN: 'Søk igjen',
NETWORK_SCANNER: 'Nettverk Scanner', NETWORK_SCANNER: 'Nettverk Scanner',
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet', NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
NETWORK_BLANK_SSID: 'la feltet være blankt for å deaktivisere trådløst nettverk',
NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID', NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID',
TX_POWER: 'Tx Effekt', TX_POWER: 'Tx Effekt',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',

View File

@@ -261,7 +261,6 @@ const pl: BaseTranslation = {
SCAN_AGAIN: 'Skanuj ponownie', SCAN_AGAIN: 'Skanuj ponownie',
NETWORK_SCANNER: 'Skaner sieci WiFi', NETWORK_SCANNER: 'Skaner sieci WiFi',
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu', NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi i włączyć ETH',
NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID', NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID',
TX_POWER: 'Moc nadawania', TX_POWER: 'Moc nadawania',
HOSTNAME: 'Nazwa w sieci', HOSTNAME: 'Nazwa w sieci',

View File

@@ -261,7 +261,6 @@ const sk: Translation = {
SCAN_AGAIN: 'Skenovať znova', SCAN_AGAIN: 'Skenovať znova',
NETWORK_SCANNER: 'Sieťový skener', NETWORK_SCANNER: 'Sieťový skener',
NETWORK_NO_WIFI: 'WiFi siete nenájdené', NETWORK_NO_WIFI: 'WiFi siete nenájdené',
NETWORK_BLANK_SSID: 'nechajte prázdne, ak chcete zakázať WiFi a povoliť ETH',
NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID', NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID',
TX_POWER: 'Tx výkon', TX_POWER: 'Tx výkon',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',

View File

@@ -261,7 +261,6 @@ const sv: Translation = {
SCAN_AGAIN: 'Sök igen', SCAN_AGAIN: 'Sök igen',
NETWORK_SCANNER: 'Hittade nätverk', NETWORK_SCANNER: 'Hittade nätverk',
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades', NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID', NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID',
TX_POWER: 'Tx effekt', TX_POWER: 'Tx effekt',
HOSTNAME: 'Värdnamn', HOSTNAME: 'Värdnamn',

View File

@@ -261,7 +261,6 @@ const tr: Translation = {
SCAN_AGAIN: 'Tekrar tara', SCAN_AGAIN: 'Tekrar tara',
NETWORK_SCANNER: 'Ağ Tarayıcısı', NETWORK_SCANNER: 'Ağ Tarayıcısı',
NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı', NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı',
NETWORK_BLANK_SSID: 'Kablosuz ağı devre dışı bırakmak için boş bırakın',
NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın', NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın',
TX_POWER: 'Aktarım gücü', TX_POWER: 'Aktarım gücü',
HOSTNAME: 'Ana Makine Adı', HOSTNAME: 'Ana Makine Adı',

View File

@@ -72,12 +72,6 @@ StateUpdateResult NetworkSettings::update(JsonObject root, NetworkSettings & set
settings.staticIPConfig = false; settings.staticIPConfig = false;
} }
// see if we need to inform the user of a restart // always best to do a restart after changing network settings
// if tx power, enableCORS, CORSOrigin, ssid changes, we need to restart return StateUpdateResult::CHANGED_RESTART;
if (tx_power != settings.tx_power || enableCORS != settings.enableCORS || CORSOrigin != settings.CORSOrigin
|| (ssid != settings.ssid && settings.ssid.isEmpty())) {
return StateUpdateResult::CHANGED_RESTART; // tell WebUI that a restart is needed
}
return StateUpdateResult::CHANGED;
} }

View File

@@ -51,7 +51,7 @@ void NetworkStatus::networkStatus(AsyncWebServerRequest * request) {
root["ssid"] = WiFi.SSID(); root["ssid"] = WiFi.SSID();
root["bssid"] = WiFi.BSSIDstr(); root["bssid"] = WiFi.BSSIDstr();
root["channel"] = WiFi.channel(); root["channel"] = WiFi.channel();
root["reconnect_count"] = emsesp::EMSESP::network_.getWifiReconnects(); root["reconnect_count"] = emsesp::EMSESP::network_.getNetworkReconnects();
root["subnet_mask"] = WiFi.subnetMask().toString(); root["subnet_mask"] = WiFi.subnetMask().toString();
if (WiFi.gatewayIP() != INADDR_NONE) { if (WiFi.gatewayIP() != INADDR_NONE) {

View File

@@ -20,14 +20,21 @@
#include "emsesp.h" #include "emsesp.h"
#ifndef NETWORK_FALLBACK_AP_SSID
#define NETWORK_FALLBACK_AP_SSID "ems-esp"
#endif
#ifndef NETWORK_FALLBACK_AP_PASSWORD
#define NETWORK_FALLBACK_AP_PASSWORD "ems-esp-neo"
#endif
namespace emsesp { namespace emsesp {
uuid::log::Logger Network::logger_{F_(network), uuid::log::Facility::KERN}; uuid::log::Logger Network::logger_{F_(network), uuid::log::Facility::KERN};
void Network::begin() { void Network::begin() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// pull Network settings and store locally on stack // pull all settings and store locally
EMSESP::esp32React.getNetworkSettingsService()->read([&](auto & settings) { EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
enableMDNS_ = settings.enableMDNS; enableMDNS_ = settings.enableMDNS;
staticIPConfig_ = settings.staticIPConfig; staticIPConfig_ = settings.staticIPConfig;
localIP_ = settings.localIP; localIP_ = settings.localIP;
@@ -65,37 +72,81 @@ void Network::begin() {
ap_subnetMask_ = settings.subnetMask; ap_subnetMask_ = settings.subnetMask;
}); });
// Initialise WiFi - we only do this once, when the network service is started. // set before begin() so the event handlers can race-clear it safely
// We want the device to come up in opmode=0 (WIFI_OFF), which is not the default after a flash erase. wifi_connect_pending_ = false;
// Persistence is true by default, so this WiFi.mode() call writes opmode=0 to NVS for future boots. ethernet_connect_pending_ = false;
WiFi.mode(WIFI_OFF);
// From here on, mode changes stay in RAM only and don't touch NVS phase_ = initialPhase();
// Initialise WiFi once when the Network service starts
WiFi.persistent(false); WiFi.persistent(false);
WiFi.setAutoReconnect(false); WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(true); // wipe old settings in NVS
WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer
WiFi.enableSTA(true); // creates the STA netif
WiFi.STA.setHostname(hostname_.c_str()); // pushes to esp_netif_set_hostname
WiFi.enableIPv6(true);
if (staticIPConfig_) {
WiFi.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); // configure for static IP
}
// www.esp32.com/viewtopic.php?t=12055
if (bandwidth20_) {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT40);
}
if (nosleep_) {
WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE
}
// scan settings give connect issues since arduino 2.0.14 and arduino 3.x.x with some wifi systems // scan settings give connect issues since arduino 2.0.14 and arduino 3.x.x with some wifi systems
// WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN // WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // default is FAST_SCAN
// WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); // is default, no need to set // WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); // is default, no need to set
// capture the WIFI_REASON_* code on every STA disconnect event so check_connection() can // avoid duplicate registration, so register the lambdas only once across the lifetime of this Network instance
// log a meaningful reason when its periodic poll notices we're no longer associated. if (!wifi_events_registered_) {
// Also release the connect-pending guard so the next loop tick can issue a fresh WiFi.begin() wifi_events_registered_ = true;
WiFi.onEvent(
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t info) { // Defer Tx power setting until STA is actually started
last_disconnect_reason_ = info.wifi_sta_disconnected.reason; WiFi.onEvent(
wifi_connect_pending_ = false; [this](WiFiEvent_t /*event*/, WiFiEventInfo_t /*info*/) {
}, #ifdef BOARD_C3_MINI_V1
ARDUINO_EVENT_WIFI_STA_DISCONNECTED); // always hardcode Tx power for Wemos C3 Mini v1
// v1 needs this value, see https://github.com/emsesp/EMS-ESP32/pull/620#discussion_r993173979
// https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
WiFi.setTxPower(WIFI_POWER_8_5dBm);
#else
setWiFiPower(tx_power_);
#endif
},
ARDUINO_EVENT_WIFI_STA_START);
// capture the WIFI_REASON_* code on every STA disconnect event so check_connection() can
// log a meaningful reason when its periodic poll notices we're no longer associated.
WiFi.onEvent(
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t info) {
last_disconnect_reason_ = info.wifi_sta_disconnected.reason;
wifi_connect_pending_ = false;
if (wifi_ever_connected_) {
LOG_WARNING("WiFi disconnected (reason: %s)", disconnectReason(last_disconnect_reason_));
} else {
LOG_WARNING("WiFi initial connect attempt failed (reason: %s)", disconnectReason(last_disconnect_reason_));
}
},
ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
// clear the saved reason and the connect-pending guard on a fresh STA association,
WiFi.onEvent(
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t /*info*/) {
last_disconnect_reason_ = 0;
wifi_connect_pending_ = false;
wifi_ever_connected_ = true;
},
ARDUINO_EVENT_WIFI_STA_GOT_IP);
}
// clear the saved reason and the connect-pending guard on a fresh STA association
WiFi.onEvent(
[this](WiFiEvent_t /*event*/, WiFiEventInfo_t /*info*/) {
last_disconnect_reason_ = 0;
wifi_connect_pending_ = false;
},
ARDUINO_EVENT_WIFI_STA_GOT_IP);
#endif #endif
} }
@@ -147,7 +198,7 @@ std::string Network::getMacAddress() const {
} }
} }
// get the number of stations connected to the AP // get the number of sessions connected to the AP
uint8_t Network::getStationNum() const { uint8_t Network::getStationNum() const {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
return network_iface_ == NetIface::AP ? WiFi.softAPgetStationNum() : 0; return network_iface_ == NetIface::AP ? WiFi.softAPgetStationNum() : 0;
@@ -159,12 +210,13 @@ uint8_t Network::getStationNum() const {
// disconnect all WiFi, Eth and AP // disconnect all WiFi, Eth and AP
// so we can starts searching again to reconnect // so we can starts searching again to reconnect
void Network::reconnect() { void Network::reconnect() {
LOG_DEBUG("Disconnecting all networks"); LOG_DEBUG("Reconnecting all networks");
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// disconnect WiFi // disconnect WiFi
if (wifi_connected()) { if (wifi_connected()) {
WiFi.disconnect(true); WiFi.disconnect(true, true);
WiFi.mode(WIFI_STA); // reset mode
} }
// disconnect AP // disconnect AP
@@ -174,17 +226,33 @@ void Network::reconnect() {
#endif #endif
// reset network state // reset network state
network_ip_ = 0; network_ip_ = 0;
network_iface_ = NetIface::NONE; network_iface_ = NetIface::NONE;
has_ipv6_ = false; has_ipv6_ = false;
juststopped_ = true; juststopped_ = false;
wifi_connect_pending_ = false; wifi_connect_pending_ = false;
last_disconnect_reason_ = 0; ethernet_connect_pending_ = false;
last_disconnect_reason_ = 0;
connect_retry_ = 0;
reconnect_count_ = 0;
phase_ = NetPhase::ETHERNET; // begin() will refine this once settings are reloaded
// reload the network settings, as this could be called from the console // reload the network settings and apply them
begin(); begin();
} }
// pick the first phase that has the hardware/config to even be attempted on this device.
// boards without an Ethernet PHY skip straight to WIFI; without a configured SSID, straight to AP
NetPhase Network::initialPhase() const {
if (phy_type_ != PHY_type::PHY_TYPE_NONE) {
return NetPhase::ETHERNET;
}
if (!ssid_.isEmpty()) {
return NetPhase::WIFI;
}
return NetPhase::AP;
}
// network loop, looking for new and disconnecting networks // network loop, looking for new and disconnecting networks
void Network::loop() { void Network::loop() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
@@ -195,17 +263,17 @@ void Network::loop() {
if (!lastConnectionAttempt_ || static_cast<uint32_t>(currentMillis - lastConnectionAttempt_) >= reconnectDelay) { if (!lastConnectionAttempt_ || static_cast<uint32_t>(currentMillis - lastConnectionAttempt_) >= reconnectDelay) {
lastConnectionAttempt_ = currentMillis; lastConnectionAttempt_ = currentMillis;
// manage network interfaces // manage the network interfaces: Ethernet, WiFi and AP in that order
startAP(); // Captive Portal (AP)
startWIFI(); // WiFi
startEthernet(); // Ethernet startEthernet(); // Ethernet
startWIFI(); // WiFi
startAP(); // Captive Portal (AP)
// already have a connection: verify it's still alive // already have a connection: verify it's still alive
// or trigger if the WiFi handshaked failed on the WiFi.begin() call
if (network_ip_ != 0 || last_disconnect_reason_ != 0) { if (network_ip_ != 0 || last_disconnect_reason_ != 0) {
checkConnection(); checkConnection();
} }
findNetworks(); // detect new connections
findNetworks(); // detect any new network connections
} }
// process DNS requests for the captive portal while the soft-AP is up // process DNS requests for the captive portal while the soft-AP is up
@@ -216,9 +284,12 @@ void Network::loop() {
} }
// Re-validate the currently active connection // Re-validate the currently active connection
// if a netif is no longer up or has lost its IP (cable unplugged, AP gone, DHCP lease lost, ...) we drop our state so // if a netif is no longer up or has lost its IP (cable unplugged, AP gone, DHCP lease lost, ...) we drop our state so find_networks() can pick up a new one
// find_networks() can pick up a new one
void Network::checkConnection() { void Network::checkConnection() {
if (network_iface_ == NetIface::NONE) {
return;
}
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
bool still_up = false; bool still_up = false;
for (esp_netif_t * netif = esp_netif_next_unsafe(NULL); netif != NULL; netif = esp_netif_next_unsafe(netif)) { for (esp_netif_t * netif = esp_netif_next_unsafe(NULL); netif != NULL; netif = esp_netif_next_unsafe(netif)) {
@@ -239,10 +310,17 @@ void Network::checkConnection() {
reason = WIFI_REASON_UNSPECIFIED; // event hasn't fired yet (or was cleared); avoid logging "0" reason = WIFI_REASON_UNSPECIFIED; // event hasn't fired yet (or was cleared); avoid logging "0"
} }
LOG_INFO("WiFi connection lost (reason %u: %s)", reason, disconnectReason(reason)); LOG_INFO("WiFi connection lost (reason %u: %s)", reason, disconnectReason(reason));
} else { wifi_connect_pending_ = false;
} else if (network_iface_ == NetIface::ETHERNET) {
LOG_INFO("Ethernet connection lost"); LOG_INFO("Ethernet connection lost");
ethernet_connect_pending_ = false;
} }
reconnect(); juststopped_ = true;
network_iface_ = NetIface::NONE;
network_ip_ = 0;
has_ipv6_ = false;
connect_retry_ = 0;
phase_ = initialPhase();
} }
#endif #endif
} }
@@ -250,12 +328,12 @@ void Network::checkConnection() {
// set the WiFi TxPower based on the RSSI (signal strength), picking the lowest value // set the WiFi TxPower based on the RSSI (signal strength), picking the lowest value
// code is based of RSSI (signal strength) and copied from Tasmota's WiFiSetTXpowerBasedOnRssi() which is copied ESPEasy's ESPEasyWifi.SetWiFiTXpower() function // code is based of RSSI (signal strength) and copied from Tasmota's WiFiSetTXpowerBasedOnRssi() which is copied ESPEasy's ESPEasyWifi.SetWiFiTXpower() function
// Range ESP32 : 2dBm - 20dBm // Range ESP32 : 2dBm - 20dBm
// 802.11b - wifi1 // 802.11b - wifi1
// 802.11a - wifi2 // 802.11a - wifi2
// 802.11g - wifi3 // 802.11g - wifi3
// 802.11n - wifi4 // 802.11n - wifi4
// 802.11ac - wifi5 // 802.11ac - wifi5
// 802.11ax - wifi6 // 802.11ax - wifi6
// tx_power is the Tx power to set, 0 for auto // tx_power is the Tx power to set, 0 for auto
void Network::setWiFiPower(uint8_t tx_power) { void Network::setWiFiPower(uint8_t tx_power) {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
@@ -269,7 +347,7 @@ void Network::setWiFiPower(uint8_t tx_power) {
int max_tx_pwr = MAX_TX_PWR_DBM_n; // assume wifi4 int max_tx_pwr = MAX_TX_PWR_DBM_n; // assume wifi4
int threshold = WIFI_SENSITIVITY_n + 120; // Margin in dBm * 10 on top of threshold int threshold = WIFI_SENSITIVITY_n + 120; // Margin in dBm * 10 on top of threshold
// Assume AP sends with max set by ETSI standard. // Assume AP sends with max set by ETSI standard
// 2.4 GHz: 100 mWatt (20 dBm) // 2.4 GHz: 100 mWatt (20 dBm)
// US and some other countries allow 1000 mW (30 dBm) // US and some other countries allow 1000 mW (30 dBm)
int rssi = WiFi.RSSI() * 10; int rssi = WiFi.RSSI() * 10;
@@ -309,7 +387,7 @@ void Network::setWiFiPower(uint8_t tx_power) {
#endif #endif
if (!WiFi.setTxPower(p)) { if (!WiFi.setTxPower(p)) {
emsesp::EMSESP::logger().warning("Failed to set WiFi Tx Power"); LOG_DEBUG("Failed to set WiFi Tx Power");
} }
#endif #endif
} }
@@ -419,88 +497,59 @@ const char * Network::disconnectReason(uint8_t code) {
// WiFi management // WiFi management
void Network::startWIFI() { void Network::startWIFI() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// Abort if already connected, or if we have no SSID or another Wifi.begin() is already in progress // only run during the WIFI phase; ETHERNET phase keeps WiFi quiet, AP phase has its own bring-up
if (WiFi.isConnected() || ssid_.length() == 0 || wifi_connect_pending_) { if (phase_ != NetPhase::WIFI) {
return; return;
} }
WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer // exit if WiFi or Ethernet is already connected, or if we have no SSID or another Wifi.begin() is already in progress
WiFi.enableSTA(true); // creates the STA netif if (WiFi.isConnected() || ssid_.isEmpty() || ethernet_connected() || wifi_connect_pending_) {
WiFi.STA.setHostname(hostname_.c_str()); // pushes to esp_netif_set_hostname return;
WiFi.enableIPv6(true);
if (staticIPConfig_) {
WiFi.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); // configure for static IP
} }
// www.esp32.com/viewtopic.php?t=12055 wifi_connect_pending_ = true;
if (bandwidth20_) {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT20);
} else {
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_STA), WIFI_BW_HT40);
}
if (nosleep_) {
WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE
}
// attempt to connect to the network LOG_DEBUG("WiFi connection with %s and %s", ssid_.c_str(), password_.c_str());
uint8_t bssid[6];
wl_status_t status;
wifi_connect_pending_ = true; // set before begin() so the event handlers can race-clear it safely
// attempt to connect to the wifi network
// the event handlers handle error handling and retries
uint8_t bssid[6];
if (formatBSSID(bssid_, bssid)) { if (formatBSSID(bssid_, bssid)) {
status = WiFi.begin(ssid_.c_str(), password_.c_str(), 0, bssid); WiFi.begin(ssid_.c_str(), password_.c_str(), 0, bssid);
} else { } else {
status = WiFi.begin(ssid_.c_str(), password_.c_str()); WiFi.begin(ssid_.c_str(), password_.c_str());
} }
if (status == WL_CONNECT_FAILED) {
wifi_connect_pending_ = false; // begin() didn't actually start anything, allow next tick to retry
LOG_ERROR("WiFi connection failed (code %d)", status);
}
#ifdef BOARD_C3_MINI_V1
// always hardcode Tx power for Wemos CS Mini v1
// v1 needs this value, see https://github.com/emsesp/EMS-ESP32/pull/620#discussion_r993173979
// https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
WiFi.setTxPower(WIFI_POWER_8_5dBm);
#else
setWiFiPower(tx_power_);
#endif
#endif #endif
} }
// Ethernet management // Ethernet management
// Brings up the ETH driver / netif exactly once. After ETH.begin() returns true the driver // Brings up the ETH driver / netif exactly once. After ETH.begin() returns true the driver
// continues to run autonomously (link negotiation, DHCP, etc); the loop must NOT call ETH.begin() // continues to run autonomously (link negotiation, DHCP, etc)
// again on every iteration because that thrashes the netif and can prevent DHCP from completing
void Network::startEthernet() { void Network::startEthernet() {
#if CONFIG_IDF_TARGET_ESP32 #if CONFIG_IDF_TARGET_ESP32
// only run during the ETHERNET phase; once we've given up on Ethernet, the driver is left
// running in case the link comes up later, but we no longer try to (re)start it here
if (phase_ != NetPhase::ETHERNET) {
return;
}
// already up and running, nothing to do // already up and running, nothing to do
if (eth_started_) { if (ethernet_connect_pending_ || ethernet_connected()) {
return; return;
} }
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// no ethernet present or wifi takes precedence // no ethernet present
if (phy_type_ == PHY_type::PHY_TYPE_NONE || (ssid_.length() > 0)) { if (phy_type_ == PHY_type::PHY_TYPE_NONE) {
return; return;
} }
// configure Ethernet // disabled Ethernet for boards with only 4MB flash and no PSRAM
int mdc = 23; // Pin# of the I²C clock signal for the Ethernet PHY - hardcoded if (ESP.getFlashChipSize() < 4194304 && !ESP.getPsramSize()) { // 4MB
int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded LOG_NOTICE("Ethernet disabled for boards with only 4MB flash");
uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) return;
int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) }
eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720
: (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110
: ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110)
// clock mode:
// ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0
// ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0
// ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16
// ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock
auto clock_mode = (eth_clock_mode_t)eth_clock_mode_;
// reset power and add a delay as ETH doesn't not always start up correctly after a warm boot // reset power and add a delay as ETH doesn't not always start up correctly after a warm boot
if (eth_power_ != -1) { if (eth_power_ != -1) {
@@ -510,15 +559,29 @@ void Network::startEthernet() {
digitalWrite(eth_power_, HIGH); digitalWrite(eth_power_, HIGH);
} }
if (ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode)) { // mdc = 23 = Pin# of the I²C clock signal for the Ethernet PHY
eth_started_ = true; // mark up; do not re-enter this block until reboot / explicit teardown // mdio = 18 = Pin# of the I²C IO signal for the Ethernet PHY
// phy_addr = I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110)
// power = Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source)
// type = Type of the Ethernet PHY (LAN8720 or TLK110)
// clock_mode =
// ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0
// ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0
// ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16
// ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock
eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720
: (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110
: ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110)
if (ETH.begin(type, eth_phy_addr_, 23, 18, eth_power_, (eth_clock_mode_t)eth_clock_mode_)) {
LOG_DEBUG("Ethernet module found - starting");
ethernet_connect_pending_ = true;
ETH.setHostname(hostname_.c_str()); // Push hostname to the ETH netif immediately after it's created ETH.setHostname(hostname_.c_str()); // Push hostname to the ETH netif immediately after it's created
ETH.enableIPv6(true); ETH.enableIPv6(true);
if (staticIPConfig_) { if (staticIPConfig_) {
ETH.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_); ETH.config(localIP_, gatewayIP_, subnetMask_, dnsIP1_, dnsIP2_);
} }
} else { } else {
LOG_ERROR("Failed to start Ethernet"); LOG_ERROR("Failed to start Ethernet module");
} }
#endif #endif
#endif #endif
@@ -526,17 +589,9 @@ void Network::startEthernet() {
// check if the network is connected and set network_ip_ / network_iface_ / has_ipv6_ // check if the network is connected and set network_ip_ / network_iface_ / has_ipv6_
// Iterates over every esp-netif that exists, prioritizing Ethernet > WiFi > AP // Iterates over every esp-netif that exists, prioritizing Ethernet > WiFi > AP
bool Network::findNetworks() { void Network::findNetworks() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// exit if already have a connection, unless in AP mode
// when in AP mode, it will always try and connect to the WiFi
if (network_ip_ != 0 && !(WiFi.getMode() & WIFI_AP)) {
// const esp_ip4_addr_t ip4 = {.addr = network_ip_};
// LOG_DEBUG("Network already connected via IPv4: " IPSTR, IP2STR(&ip4));
return true;
}
struct NetInfo { struct NetInfo {
esp_ip4_addr_t ip; esp_ip4_addr_t ip;
esp_ip6_addr_t ip6; esp_ip6_addr_t ip6;
@@ -580,23 +635,31 @@ bool Network::findNetworks() {
strlcpy(info.desc, desc, sizeof(info.desc)); strlcpy(info.desc, desc, sizeof(info.desc));
} }
info.has_ipv6 = (esp_netif_get_ip6_linklocal(netif, &info.ip6) == ESP_OK); info.has_ipv6 = (esp_netif_get_ip6_linklocal(netif, &info.ip6) == ESP_OK);
best_iface = candidate;
best_iface = candidate;
if (best_iface == NetIface::ETHERNET) { if (best_iface == NetIface::ETHERNET) {
break; // top priority, can't be beaten by anything later in the list break; // top priority, can't be beaten by anything later in the list
} }
} }
auto previous_iface = NetIface::NONE;
// if we have a connection and it's a new one, set it up // if we have a connection and it's a new one, set it up
if (best_iface != NetIface::NONE && best_iface != previous_iface) { if (best_iface != NetIface::NONE && best_iface != network_iface_) {
previous_iface = network_iface_; // save the previous interface for comparison next time
network_ip_ = info.ip.addr; network_ip_ = info.ip.addr;
network_iface_ = iface_from_desc(info.desc); // "sta"/"ap"/"eth*" network_iface_ = iface_from_desc(info.desc); // "sta"/"ap"/"eth*"
has_ipv6_ = info.has_ipv6; has_ipv6_ = info.has_ipv6;
connect_retry_ = 0; connect_retry_ = 0;
// sync the phase to the interface that actually came up. ETH can come up late
// (e.g. cable plugged in after we'd already moved on to WiFi/AP) and we want the
// next disconnect-driven retry cycle to start from the right place.
if (network_iface_ == NetIface::ETHERNET) {
phase_ = NetPhase::ETHERNET;
} else if (network_iface_ == NetIface::WIFI) {
phase_ = NetPhase::WIFI;
} else if (network_iface_ == NetIface::AP) {
phase_ = NetPhase::AP;
}
LOG_INFO("Network connected via %s (IP: " IPSTR ")", LOG_INFO("Network connected via %s (IP: " IPSTR ")",
network_iface_ == NetIface::ETHERNET ? "Ethernet" network_iface_ == NetIface::ETHERNET ? "Ethernet"
: network_iface_ == NetIface::WIFI ? "WiFi" : network_iface_ == NetIface::WIFI ? "WiFi"
@@ -612,8 +675,7 @@ bool Network::findNetworks() {
// count the number of restarts (for Wifi and Eth) // count the number of restarts (for Wifi and Eth)
if (juststopped_) { if (juststopped_) {
juststopped_ = false; juststopped_ = false;
connectcount_++; reconnect_count_++;
LOG_DEBUG("Network re-connection count %d", connectcount_);
} }
// start mDNS for any real network interface (skip the SoftAP since the captive portal handles its own DNS) // start mDNS for any real network interface (skip the SoftAP since the captive portal handles its own DNS)
@@ -624,44 +686,94 @@ bool Network::findNetworks() {
// fetch the versions.json file from emsesp.org // fetch the versions.json file from emsesp.org
EMSESP::webStatusService.schedule_versions_refresh(); EMSESP::webStatusService.schedule_versions_refresh();
return true; // we have a network connection return;
}
// fallback, reset network state if nothing found
if (best_iface == NetIface::NONE) {
network_ip_ = 0;
network_iface_ = NetIface::NONE;
has_ipv6_ = false;
connect_retry_++;
LOG_DEBUG("Looking for network interfaces (retry #%d, phase=%s)",
connect_retry_,
phase_ == NetPhase::ETHERNET ? "Ethernet"
: phase_ == NetPhase::WIFI ? "WiFi"
: "AP");
// give up on this interface and try the next one. ETHERNET -> WIFI (or straight to AP if no SSID configured); WIFI -> AP.
if (connect_retry_ >= MAX_NETWORK_RECONNECTION_ATTEMPTS && phase_ != NetPhase::AP) {
if (phase_ == NetPhase::ETHERNET) {
if (ssid_.isEmpty()) {
LOG_WARNING("Ethernet failed to connect after %u attempts, falling back to AP", connect_retry_);
phase_ = NetPhase::AP;
} else {
LOG_WARNING("Ethernet failed to connect after %u attempts, switching to WiFi", connect_retry_);
phase_ = NetPhase::WIFI;
}
ethernet_connect_pending_ = false;
} else if (phase_ == NetPhase::WIFI) {
LOG_WARNING("WiFi failed to connect after %u attempts, falling back to AP", connect_retry_);
phase_ = NetPhase::AP;
wifi_connect_pending_ = false;
WiFi.disconnect(true);
}
connect_retry_ = 0;
}
} }
// fallback
network_ip_ = 0;
network_iface_ = NetIface::NONE;
has_ipv6_ = false;
connect_retry_++;
LOG_DEBUG("No active network interfaces found yet, re-connection count %d", connect_retry_);
#endif #endif
return false; // no connection found yet return;
} }
// access point (soft-AP) and the captive portal // access point (soft-AP) and the captive portal
void Network::startAP() { void Network::startAP() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
// Only start AP as a fallback if the Network has failed // only start AP as a fallback once both the Ethernet and WiFi phases have given up
if (connect_retry_ < MAX_NETWORK_RECONNECTION_ATTEMPTS) { if (phase_ != NetPhase::AP) {
return; return;
} }
// don't start the soft-AP if it is disabled, or Ethernet has taken over or we have a real WiFi connection or it's already running // Don't start AP if wired/Wi-Fi STA is serving the network
if (ap_provisionMode_ == AP_MODE_NEVER || network_connected() || WiFi.getMode() & WIFI_AP) { if (ap_provisionMode_ == AP_MODE_NEVER || network_connected()) {
return; return;
} }
WiFi.softAPenableIPv6(); // force IPv6, same as for STA - fixes https://github.com/emsesp/EMS-ESP32/issues/1922 // Captive-portal DNS is already bound to the softAP interface
WiFi.softAPConfig(ap_localIP_, ap_gatewayIP_, ap_subnetMask_); if (ap_dnsServer_) {
return;
}
if (!WiFi.softAPConfig(ap_localIP_, ap_gatewayIP_, ap_subnetMask_)) {
LOG_DEBUG("softAPConfig failed");
return;
}
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_AP), WIFI_BW_HT20); esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_AP), WIFI_BW_HT20);
WiFi.softAP(ap_ssid_.c_str(), ap_password_.c_str(), ap_channel_, ap_ssid_hidden_, ap_max_clients_);
// WiFi.softAPenableIPv6();
if (!WiFi.softAP(ap_ssid_.c_str(), ap_password_.c_str(), ap_channel_, ap_ssid_hidden_, ap_max_clients_)) {
LOG_ERROR("softAP failed; check SSID/password in AP settings");
WiFi.softAPdisconnect(true);
return;
}
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi WiFi.setTxPower(WIFI_POWER_8_5dBm); // https://www.wemos.cc/en/latest/c3/c3_mini_1_0_0.html#about-wifi
#endif #endif
const IPAddress apIp = WiFi.softAPIP(); const IPAddress apIp = WiFi.softAPIP();
if (static_cast<uint32_t>(apIp) == 0) {
LOG_DEBUG("SoftAP has no IPv4 yet; skipping captive-portal DNS for now.");
WiFi.softAPdisconnect(true);
return;
}
LOG_INFO("Starting Access Point with captive portal on %u.%u.%u.%u", apIp[0], apIp[1], apIp[2], apIp[3]); LOG_INFO("Starting Access Point with captive portal on %u.%u.%u.%u", apIp[0], apIp[1], apIp[2], apIp[3]);
// start DNS server for Captive Portal
ap_dnsServer_ = new DNSServer; ap_dnsServer_ = new DNSServer;
if (!ap_dnsServer_) {
LOG_DEBUG("Out of memory starting captive-portal DNSServer");
WiFi.softAPdisconnect(true);
return;
}
ap_dnsServer_->start(DNS_PORT, "*", apIp); ap_dnsServer_->start(DNS_PORT, "*", apIp);
#endif #endif
} }

View File

@@ -35,12 +35,17 @@
namespace emsesp { namespace emsesp {
#define NETWORK_RECONNECTION_DELAY_SHORT 3000 // 3 seconds #define NETWORK_RECONNECTION_DELAY_SHORT 5000 // 5 seconds
#define NETWORK_RECONNECTION_DELAY_LONG 60000 // 60 seconds
#define MAX_NETWORK_RECONNECTION_ATTEMPTS 3 // maximum number of network reconnection attempts #ifndef EMSESP_DEBUG
#define NETWORK_RECONNECTION_DELAY_LONG 60000 // 1 minute
#else
#define NETWORK_RECONNECTION_DELAY_LONG 10000 // 10 seconds - for debugging
#endif
#define DNS_PORT 53 #define MAX_NETWORK_RECONNECTION_ATTEMPTS 3 // maximum number of network reconnection attempts before going to AP fallback
#define DNS_PORT 53 // dns server port for captive portal
// copied from Tasmota // copied from Tasmota
#if CONFIG_IDF_TARGET_ESP32S2 #if CONFIG_IDF_TARGET_ESP32S2
@@ -75,13 +80,18 @@ namespace emsesp {
// which physical interface we are currently using for the active network connection. // which physical interface we are currently using for the active network connection.
// Mapped from the esp-netif description string returned by esp_netif_get_desc(): "sta" -> WIFI, // Mapped from the esp-netif description string returned by esp_netif_get_desc(): "sta" -> WIFI,
// "ap" -> AP, "eth"/"eth1"/"eth2"/... (arduino-esp32 v3.x suffixes ETH netifs because it supports // "ap" -> AP, "eth"/"eth1"/"eth2"/...
// multiple ETH instances) -> ETHERNET. Anything else stays as NONE.
enum class NetIface : uint8_t { enum class NetIface : uint8_t {
NONE = 0, NONE = 0, // 0
WIFI, WIFI, // 1
ETHERNET, ETHERNET, // 2
AP, AP, // 3
};
enum class NetPhase : uint8_t {
ETHERNET = 0,
WIFI = 1,
AP = 2,
}; };
class Network { class Network {
@@ -89,10 +99,6 @@ class Network {
void begin(); void begin();
void loop(); void loop();
uint16_t getWifiReconnects() const {
return connectcount_;
}
uint32_t network_ip() const { uint32_t network_ip() const {
return network_ip_; return network_ip_;
} }
@@ -121,8 +127,8 @@ class Network {
return has_ipv6_ && (network_iface_ == NetIface::WIFI || network_iface_ == NetIface::ETHERNET); return has_ipv6_ && (network_iface_ == NetIface::WIFI || network_iface_ == NetIface::ETHERNET);
} }
uint16_t getWifiReconnects() { uint16_t getNetworkReconnects() {
return connectcount_; return reconnect_count_;
} }
std::string getLocalIP() const; std::string getLocalIP() const;
@@ -149,10 +155,25 @@ class Network {
return NetIface::NONE; return NetIface::NONE;
} }
static const char * network_iface_to_string(NetIface iface) {
switch (iface) {
case NetIface::WIFI:
return "WiFi";
case NetIface::ETHERNET:
return "Ethernet";
case NetIface::AP:
return "AP";
case NetIface::NONE:
default:
return "None";
}
return "unknown";
}
private: private:
static uuid::log::Logger logger_; static uuid::log::Logger logger_;
bool findNetworks(); void findNetworks();
void checkConnection(); void checkConnection();
void startmDNS() const; void startmDNS() const;
bool formatBSSID(const String & bssid, uint8_t (&mac)[6]); bool formatBSSID(const String & bssid, uint8_t (&mac)[6]);
@@ -162,6 +183,7 @@ class Network {
void setWiFiPower(uint8_t tx_power); void setWiFiPower(uint8_t tx_power);
const char * disconnectReason(uint8_t code); const char * disconnectReason(uint8_t code);
void stopAP(); void stopAP();
NetPhase initialPhase() const;
#if defined(__clang__) #if defined(__clang__)
#pragma clang diagnostic push #pragma clang diagnostic push
@@ -169,15 +191,21 @@ class Network {
#endif #endif
unsigned long lastConnectionAttempt_ = 0; unsigned long lastConnectionAttempt_ = 0;
uint16_t connectcount_ = 0; // number of network reconnects uint16_t reconnect_count_ = 0; // number of network reconnects
uint32_t network_ip_ = 0; uint32_t network_ip_ = 0;
NetIface network_iface_ = NetIface::NONE; NetIface network_iface_ = NetIface::NONE;
bool has_ipv6_ = false; bool has_ipv6_ = false;
bool juststopped_ = false; bool juststopped_ = false;
bool eth_started_ = false; // true after ETH.begin() has succeeded once; prevents repeated re-init while DHCP is still running
volatile uint8_t last_disconnect_reason_ = 0; volatile uint8_t last_disconnect_reason_ = 0;
uint16_t connect_retry_ = 0; // number of network re-connection attempts uint16_t connect_retry_ = 0; // number of network re-connection attempts
volatile bool wifi_connect_pending_ = false;
volatile bool wifi_connect_pending_ = false;
volatile bool ethernet_connect_pending_ = false;
NetPhase phase_ = NetPhase::ETHERNET;
bool wifi_events_registered_ = false; // ensure WiFi.onEvent() handlers are registered only once across begin()/reconnect() cycles
bool wifi_ever_connected_ = false; // set true once we've successfully obtained an IP
// Network and AP settings // Network and AP settings
bool enableMDNS_; bool enableMDNS_;
@@ -216,7 +244,7 @@ class Network {
// for the captive portal in AP mode // for the captive portal in AP mode
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
DNSServer * ap_dnsServer_; DNSServer * ap_dnsServer_ = nullptr;
#endif #endif
}; };

View File

@@ -742,6 +742,8 @@ void System::button_OnClick(PButton & b) {
} }
// button double click // button double click
// reconnect to AP by removing the SSID from the network settings
// note: in v3.9 this is normal behaviour to fallback to AP if the Wifi or Ethernet connection fails
void System::button_OnDblClick(PButton & b) { void System::button_OnDblClick(PButton & b) {
LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); LOG_NOTICE("Button pressed - double click - wifi reconnect to AP");
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
@@ -998,7 +1000,7 @@ void System::heartbeat_json(JsonObject output) {
int8_t rssi = WiFi.RSSI(); int8_t rssi = WiFi.RSSI();
output["rssi"] = rssi; output["rssi"] = rssi;
output["wifistrength"] = wifi_quality(rssi); output["wifistrength"] = wifi_quality(rssi);
output["wifireconnects"] = EMSESP::network_.getWifiReconnects(); output["wifireconnects"] = EMSESP::network_.getNetworkReconnects();
} }
#endif #endif
@@ -1307,19 +1309,19 @@ void System::show_system(uuid::console::Shell & shell) {
shell.println("Network:"); shell.println("Network:");
switch (WiFi.status()) { switch (WiFi.status()) {
case WL_IDLE_STATUS: case WL_IDLE_STATUS:
shell.printfln(" Status: Idle"); shell.printfln(" WiFi Status: Idle");
break; break;
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
shell.printfln(" Status: Network not found"); shell.printfln(" WiFi Status: Network not found");
break; break;
case WL_SCAN_COMPLETED: case WL_SCAN_COMPLETED:
shell.printfln(" Status: Network scan complete"); shell.printfln(" WiFi Status: Network scan complete");
break; break;
case WL_CONNECTED: case WL_CONNECTED:
shell.printfln(" Status: WiFi connected"); shell.printfln(" WiFi Status: Connected");
shell.printfln(" SSID: %s", WiFi.SSID().c_str()); shell.printfln(" SSID: %s", WiFi.SSID().c_str());
shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str());
shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI()));
@@ -1367,8 +1369,13 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str());
} }
} }
shell.println();
// show AP is connected
if (EMSESP::network_.ap_connected()) {
shell.printfln(" AP Status: connected");
}
shell.println();
shell.println("Syslog:"); shell.println("Syslog:");
if (!syslog_enabled_) { if (!syslog_enabled_) {
shell.printfln(" Syslog: disabled"); shell.printfln(" Syslog: disabled");
@@ -2451,7 +2458,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
node["network"] = "WiFi"; node["network"] = "WiFi";
node["hostname"] = WiFi.getHostname(); node["hostname"] = WiFi.getHostname();
node["RSSI"] = WiFi.RSSI(); node["RSSI"] = WiFi.RSSI();
node["WIFIReconnects"] = EMSESP::network_.getWifiReconnects(); node["WIFIReconnects"] = EMSESP::network_.getNetworkReconnects();
// node["MAC"] = WiFi.macAddress(); // node["MAC"] = WiFi.macAddress();
// node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask()); // node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
// node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); // node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());

View File

@@ -359,7 +359,6 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
// either via the Web UI or via the Console // either via the Web UI or via the Console
void WebSettingsService::onUpdate() { void WebSettingsService::onUpdate() {
// skip if we're restarting anyway // skip if we're restarting anyway
if (WebSettings::has_flags(WebSettings::ChangeFlags::RESTART)) { if (WebSettings::has_flags(WebSettings::ChangeFlags::RESTART)) {
return; return;
} }

View File

@@ -369,6 +369,18 @@ void WebStatusService::getVersions(JsonObject root) {
#endif #endif
} }
// schedule the next versions.json fetch a few seconds out so the network stack has time to settle
// (DHCP completion, default-netif assignment and DNS server propagation through lwip)
void WebStatusService::schedule_versions_refresh() {
#ifndef EMSESP_STANDALONE
uint32_t next = uuid::get_uptime() + VERSIONS_INITIAL_FETCH_DELAY_MS;
if (next == 0) {
next = 1; // 0 is the "idle" sentinel — never let the wrap land there
}
versions_next_fetch_ms_ = next;
#endif
}
// periodic refresh (1 hour) of the cached versions.json // periodic refresh (1 hour) of the cached versions.json
// runs on the main loop task, which has a much bigger stack than AsyncTCP needed for https // runs on the main loop task, which has a much bigger stack than AsyncTCP needed for https
void WebStatusService::loop() { void WebStatusService::loop() {
@@ -378,8 +390,6 @@ void WebStatusService::loop() {
return; return;
} }
// TODO handle a network re-connect to fetch the values again (set versions_next_fetch_ms_ to 1)
// 0 = idle, nothing scheduled // 0 = idle, nothing scheduled
if (versions_next_fetch_ms_ == 0) { if (versions_next_fetch_ms_ == 0) {
return; return;
@@ -419,7 +429,7 @@ bool WebStatusService::refresh_versions_cache() {
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) { if (httpCode != HTTP_CODE_OK) {
#if defined(EMSESP_DEBUG) #if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: HTTP %d", httpCode); EMSESP::logger().debug("versions.json: HTTP error code %d", httpCode);
#endif #endif
http.end(); http.end();
return false; return false;
@@ -453,7 +463,7 @@ bool WebStatusService::refresh_versions_cache() {
versions_cache_valid_ = true; versions_cache_valid_ = true;
#if defined(EMSESP_DEBUG) #if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: refreshed (stable=%s dev=%s), current=%s", EMSESP::logger().debug("versions.json: fetched stable=%s, dev=%s, current=%s",
versions_stable_.version.c_str(), versions_stable_.version.c_str(),
versions_dev_.version.c_str(), versions_dev_.version.c_str(),
current_version_s.c_str()); current_version_s.c_str());

View File

@@ -30,10 +30,11 @@ class WebStatusService {
return versions_cache_valid_; return versions_cache_valid_;
} }
// refresh the versions.json cache // schedule a refresh of the versions.json cache. Defers the fetch by
void schedule_versions_refresh() { // VERSIONS_INITIAL_FETCH_DELAY_MS so the network stack (DHCP, default netif, DNS server)
versions_next_fetch_ms_ = 1; // has time to settle after the link first comes up — otherwise hostByName() can fail
} // immediately on boot with a noisy "DNS Failed ... error '-54'".
void schedule_versions_refresh();
bool current_upgradeable() const; // true if a newer version is available bool current_upgradeable() const; // true if a newer version is available
@@ -71,8 +72,9 @@ class WebStatusService {
bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success
static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure
static constexpr uint32_t VERSIONS_INITIAL_FETCH_DELAY_MS = 5UL * 1000UL; // 5 s after a link comes up
}; };
} // namespace emsesp } // namespace emsesp