feat: add automatic network failover

This commit is contained in:
agessaman committed 2026-09-01 21:18:22 -07:00
1 parent 03c0ad81fe
commit 366b086713
23 files changed
+600 -86

No files matched your search

+23 -18
View File
@@ -4,7 +4,7 @@
#include <stdlib.h> // for qsort()
#include <helpers/RxReservePacketManager.h>
#include <helpers/NetworkInterface.h>
#if defined(ESP_PLATFORM) && !defined(NETWORK_USE_ETHERNET)
#if defined(ESP_PLATFORM)
#include <WiFi.h>
#endif
#if defined(WITH_MQTT_NEIGHBORS)
@@ -1147,6 +1147,17 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
NetworkInterface& boot_network = activeNetworkInterface();
if (boot_network.isAutomatic()) {
MQTTPrefs* obs = _cli.getObserverPrefs();
Serial.printf("Network: probing Ethernet for up to %lums\n",
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
boot_network.bootstrap(obs->wifi_ssid, obs->wifi_password,
NETWORK_ETHERNET_BOOT_WAIT_MS);
Serial.printf("Network: selected %s (%s)\n", boot_network.mediumName(),
boot_network.statusName());
}
acl.load(_fs, self_id);
// TODO: key_store.begin();
region_map.load(_fs);
@@ -1221,15 +1232,13 @@ void MyMesh::begin(FILESYSTEM *fs) {
#endif
#if defined(WITH_WEBCONFIG) && !defined(WEBCONFIG_NO_AUTO_AP)
// First-boot setup portal: raised only when no WiFi has ever been configured,
// so an OTA onto a deployed (configured) node can never open an AP.
#if !defined(NETWORK_USE_ETHERNET)
if (_cli.getObserverPrefs()->wifi_ssid[0] == 0) {
// Existing Wi-Fi installs are complete by virtue of their stored SSID. An
// Ethernet-only install uses the explicit completion marker instead.
if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
char wc_reply[160];
startWebConfig(false, wc_reply);
Serial.println(wc_reply);
}
#endif
#endif
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
@@ -1474,11 +1483,6 @@ void MyMesh::clearStats() {
#ifdef WITH_WEBCONFIG
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
#if defined(NETWORK_USE_ETHERNET)
(void)force_ap;
strcpy(reply, "Err: webconfig unavailable on Ethernet observer; use serial CLI");
return true;
#else
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
strcpy(reply, _webconfig->isStopping() ? "Err: webconfig still stopping, retry shortly"
: "Err: webconfig already running");
@@ -1491,18 +1495,21 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
}
if (force_ap) {
// The setup AP owns WiFi outright; refuse while the bridge holds the STA.
if (bridge && bridge->isRunning()) {
if (bridge && bridge->isRunning() &&
activeNetworkInterface().medium() != NetworkMedium::Ethernet) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
_webconfig->startSetupMode(reply);
} else if (_cli.getObserverPrefs()->wifi_ssid[0] == 0) {
_webconfig->startSetupMode(reply); // unconfigured: same portal as first boot
} else if (activeNetworkInterface().isConnected()) {
_webconfig->startLanMode(activeNetworkInterface().localIP(),
!mqttNetworkSetupComplete(_cli.getObserverPrefs()), reply);
} else if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
_webconfig->startSetupMode(reply);
} else {
_webconfig->startLanMode(reply); // reports "WiFi not connected" if down
strcpy(reply, "Err: selected network not connected");
}
return true;
#endif
}
bool MyMesh::stopWebConfig(char* reply) {
@@ -1542,11 +1549,9 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
const int signal = network.rssi();
wifi_rssi = signal == INT_MIN ? 0 : signal;
}
#if !defined(NETWORK_USE_ETHERNET)
else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
}
#endif
int pos = snprintf(buf, buf_size,
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
+7
View File
@@ -507,6 +507,13 @@ public:
_wc_slot_restart_mask = 0;
}
void onConfigBatchEnd() override;
bool onInitialSetupComplete() override {
MQTTPrefs* obs = _cli.getObserverPrefs();
obs->network_setup_complete = 1;
if (_cli.saveObserverPrefs(_fs)) return true;
obs->network_setup_complete = 0;
return false;
}
void buildStatsJson(char* buf, size_t buf_size) override;
#endif
+3 -2
View File
@@ -174,15 +174,16 @@ void UITask::renderCurrScreen() {
}
char wc_ssid[33], wc_ip[16];
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
const bool lan_setup = WebConfigServer::isLanSetup();
// setup portal active: show join instructions instead of the home screen
_display->setTextSize(1);
_display->setColor(UIColor::corp_blue);
_display->setCursor(0, 0);
_display->print("Observer WiFi Setup");
_display->print(lan_setup ? "Observer Ethernet Setup" : "Observer WiFi Setup");
_display->setColor(UIColor::primary_txt);
_display->setCursor(0, 14);
_display->print("Join WiFi:");
_display->print(lan_setup ? "Login code:" : "Join WiFi:");
_display->setColor(UIColor::warning_txt);
_display->setCursor(6, 24);
_display->print(wc_ssid);
+23 -18
View File
@@ -3,7 +3,7 @@
#include <climits>
#include <helpers/RxReservePacketManager.h>
#include <helpers/NetworkInterface.h>
#if defined(ESP_PLATFORM) && !defined(NETWORK_USE_ETHERNET)
#if defined(ESP_PLATFORM)
#include <WiFi.h>
#endif
#if defined(WITH_MQTT_NEIGHBORS)
@@ -948,6 +948,17 @@ void MyMesh::begin(FILESYSTEM *fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
NetworkInterface& boot_network = activeNetworkInterface();
if (boot_network.isAutomatic()) {
MQTTPrefs* obs = _cli.getObserverPrefs();
Serial.printf("Network: probing Ethernet for up to %lums\n",
(unsigned long)NETWORK_ETHERNET_BOOT_WAIT_MS);
boot_network.bootstrap(obs->wifi_ssid, obs->wifi_password,
NETWORK_ETHERNET_BOOT_WAIT_MS);
Serial.printf("Network: selected %s (%s)\n", boot_network.mediumName(),
boot_network.statusName());
}
acl.load(_fs, self_id);
region_map.load(_fs);
@@ -1023,15 +1034,13 @@ void MyMesh::begin(FILESYSTEM *fs) {
#endif
#if defined(WITH_WEBCONFIG) && !defined(WEBCONFIG_NO_AUTO_AP)
// First-boot setup portal: raised only when no WiFi has ever been configured,
// so an OTA onto a deployed (configured) node can never open an AP.
#if !defined(NETWORK_USE_ETHERNET)
if (_cli.getObserverPrefs()->wifi_ssid[0] == 0) {
// Existing Wi-Fi installs are complete by virtue of their stored SSID. An
// Ethernet-only install uses the explicit completion marker instead.
if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
char wc_reply[160];
startWebConfig(false, wc_reply);
Serial.println(wc_reply);
}
#endif
#endif
}
@@ -1276,11 +1285,6 @@ void MyMesh::formatPacketStatsReply(char *reply) {
#ifdef WITH_WEBCONFIG
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
#if defined(NETWORK_USE_ETHERNET)
(void)force_ap;
strcpy(reply, "Err: webconfig unavailable on Ethernet observer; use serial CLI");
return true;
#else
if (_webconfig && (_webconfig->isRunning() || _webconfig->isStopping())) {
strcpy(reply, _webconfig->isStopping() ? "Err: webconfig still stopping, retry shortly"
: "Err: webconfig already running");
@@ -1293,18 +1297,21 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
}
if (force_ap) {
// The setup AP owns WiFi outright; refuse while the bridge holds the STA.
if (bridge && bridge->isRunning()) {
if (bridge && bridge->isRunning() &&
activeNetworkInterface().medium() != NetworkMedium::Ethernet) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
_webconfig->startSetupMode(reply);
} else if (_cli.getObserverPrefs()->wifi_ssid[0] == 0) {
_webconfig->startSetupMode(reply); // unconfigured: same portal as first boot
} else if (activeNetworkInterface().isConnected()) {
_webconfig->startLanMode(activeNetworkInterface().localIP(),
!mqttNetworkSetupComplete(_cli.getObserverPrefs()), reply);
} else if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
_webconfig->startSetupMode(reply);
} else {
_webconfig->startLanMode(reply); // reports "WiFi not connected" if down
strcpy(reply, "Err: selected network not connected");
}
return true;
#endif
}
bool MyMesh::stopWebConfig(char* reply) {
@@ -1344,11 +1351,9 @@ void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
const int signal = network.rssi();
wifi_rssi = signal == INT_MIN ? 0 : signal;
}
#if !defined(NETWORK_USE_ETHERNET)
else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
}
#endif
int pos = snprintf(buf, buf_size,
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
+7
View File
@@ -497,6 +497,13 @@ public:
_wc_slot_restart_mask = 0;
}
void onConfigBatchEnd() override;
bool onInitialSetupComplete() override {
MQTTPrefs* obs = _cli.getObserverPrefs();
obs->network_setup_complete = 1;
if (_cli.saveObserverPrefs(_fs)) return true;
obs->network_setup_complete = 0;
return false;
}
void buildStatsJson(char* buf, size_t buf_size) override;
#endif
};
+3 -2
View File
@@ -155,15 +155,16 @@ void UITask::renderCurrScreen() {
}
char wc_ssid[33], wc_ip[16];
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
const bool lan_setup = WebConfigServer::isLanSetup();
// setup portal active: show join instructions instead of the home screen
_display->setTextSize(1);
_display->setColor(UIColor::corp_blue);
_display->setCursor(0, 0);
_display->print("Observer WiFi Setup");
_display->print(lan_setup ? "Observer Ethernet Setup" : "Observer WiFi Setup");
_display->setColor(UIColor::primary_txt);
_display->setCursor(0, 14);
_display->print("Join WiFi:");
_display->print(lan_setup ? "Login code:" : "Join WiFi:");
_display->setColor(UIColor::warning_txt);
_display->setCursor(6, 24);
_display->print(wc_ssid);