fix(network): pre-merge review fixes

- Guard the boot-time link bootstrap to observer ESP32 builds. It ran
  unconditionally in MyMesh::begin(), breaking every non-observer
  repeater/room server build (ESP32 and nRF52).
- Rename NetworkInterface -> NetworkLink (class, accessor, files).
  Arduino-ESP32 3.x ships its own NetworkInterface class and header,
  which broke the ESP32-C6 builds. Drop WiFi.setAutoConnect(), a no-op
  on 2.x and removed in 3.x.
- Refresh stored Wi-Fi credentials every bridge tick so the STA
  reconnect loop picks up `set wifi.ssid` / `set wifi.pwd` without a
  reboot, as the bridge did before the link moved out of it. Skip
  reconnects while the SSID is empty.
- Restore the "WiFi connected: <ip>" / "WiFi disconnected: reason N"
  debug lines the bridge used to print.
- Alert on Ethernet only once it has held a lease this boot or when no
  Wi-Fi is configured; Wi-Fi-only installs of an Ethernet-preferred
  image keep Wi-Fi alerts instead of reporting "Ethernet down".
- Record wifi.setup_complete only for Ethernet LAN onboarding, so
  Wi-Fi builds keep the SSID-based first-boot portal rule.
- Use seq_cst for the route-switch lock/mutation flag handshake.
- Docs: SNMP RSSI sentinel is -127; describe link-return vs medium-switch
  reconnect behavior accurately; note runtime credential pickup.
- Test: unknown keys inside a known /mqtt.json group are ignored, which
  keeps wifi.setup_complete downgrade-safe.
This commit is contained in:
agessaman committed 2026-09-19 10:14:16 -07:00
1 parent 8ab6a988aa
commit a129420804
14 files changed
+149 -95

No files matched your search

+5 -2
View File
@@ -872,8 +872,11 @@ the radio actually performs in that case.
- CH390 startup initializes Arduino's shared network event runtime without associating WiFi;
this keeps the framework's DNS and TLS hostname paths safe when Ethernet wins directly.
- Ethernet/WiFi transitions are logged. A lost or changed route stops every started MQTT
client, including one whose disconnect callback arrived first; once a usable route returns,
route-caused backoff is cleared and one immediate reconnect is allowed
client, including one whose disconnect callback arrived first. When the same link returns,
each slot gets one immediate attempt at its current backoff rung (a tripped circuit breaker
gets one immediate probe); a switch to the other medium also clears backoff and breakers
- WiFi credentials changed at runtime (`set wifi.ssid` / `set wifi.pwd`) are used on the next
reconnect attempt without a reboot
- Packets are queued while a slot is disconnected and flushed when it recovers
### Raw Radio Data Capture
+8 -6
View File
@@ -3,7 +3,7 @@
#include <climits>
#include <stdlib.h> // for qsort()
#include <helpers/RxReservePacketManager.h>
#include <helpers/NetworkInterface.h>
#include <helpers/NetworkLink.h>
#include <helpers/NetworkHostname.h>
#if defined(ESP_PLATFORM)
#include <WiFi.h>
@@ -1148,7 +1148,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
NetworkInterface& boot_network = activeNetworkInterface();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP_PLATFORM)
NetworkLink& boot_network = activeNetworkLink();
if (boot_network.isAutomatic()) {
char network_hostname[NetworkHostname::kBufferSize];
NetworkHostname::build(network_hostname, sizeof(network_hostname),
@@ -1165,6 +1166,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
boot_network.mediumName(), boot_network.statusName(),
(unsigned long)(millis() - ethernet_probe_started_at));
}
#endif
acl.load(_fs, self_id);
// TODO: key_store.begin();
@@ -1504,13 +1506,13 @@ 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() &&
activeNetworkInterface().medium() != NetworkMedium::Ethernet) {
activeNetworkLink().medium() != NetworkMedium::Ethernet) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
_webconfig->startSetupMode(reply);
} else if (activeNetworkInterface().isConnected()) {
_webconfig->startLanMode(activeNetworkInterface().localIP(),
} else if (activeNetworkLink().isConnected()) {
_webconfig->startLanMode(activeNetworkLink().localIP(),
!mqttNetworkSetupComplete(_cli.getObserverPrefs()), reply);
} else if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
_webconfig->startSetupMode(reply);
@@ -1551,7 +1553,7 @@ void MyMesh::onConfigBatchEnd() {
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
char ip[20] = "";
char wifi_rssi[12] = "null";
NetworkInterface& network = activeNetworkInterface();
NetworkLink& network = activeNetworkLink();
const char* network_medium = network.mediumName();
if (network.isConnected()) {
strncpy(ip, network.localIP().toString().c_str(), sizeof(ip) - 1);
+8 -6
View File
@@ -2,7 +2,7 @@
#include <algorithm>
#include <climits>
#include <helpers/RxReservePacketManager.h>
#include <helpers/NetworkInterface.h>
#include <helpers/NetworkLink.h>
#include <helpers/NetworkHostname.h>
#if defined(ESP_PLATFORM)
#include <WiFi.h>
@@ -949,7 +949,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
NetworkInterface& boot_network = activeNetworkInterface();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP_PLATFORM)
NetworkLink& boot_network = activeNetworkLink();
if (boot_network.isAutomatic()) {
char network_hostname[NetworkHostname::kBufferSize];
NetworkHostname::build(network_hostname, sizeof(network_hostname),
@@ -966,6 +967,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
boot_network.mediumName(), boot_network.statusName(),
(unsigned long)(millis() - ethernet_probe_started_at));
}
#endif
acl.load(_fs, self_id);
region_map.load(_fs);
@@ -1306,13 +1308,13 @@ 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() &&
activeNetworkInterface().medium() != NetworkMedium::Ethernet) {
activeNetworkLink().medium() != NetworkMedium::Ethernet) {
strcpy(reply, "Err: MQTT bridge is running - 'set bridge off' first");
return true;
}
_webconfig->startSetupMode(reply);
} else if (activeNetworkInterface().isConnected()) {
_webconfig->startLanMode(activeNetworkInterface().localIP(),
} else if (activeNetworkLink().isConnected()) {
_webconfig->startLanMode(activeNetworkLink().localIP(),
!mqttNetworkSetupComplete(_cli.getObserverPrefs()), reply);
} else if (!mqttNetworkSetupComplete(_cli.getObserverPrefs())) {
_webconfig->startSetupMode(reply);
@@ -1353,7 +1355,7 @@ void MyMesh::onConfigBatchEnd() {
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
char ip[20] = "";
char wifi_rssi[12] = "null";
NetworkInterface& network = activeNetworkInterface();
NetworkLink& network = activeNetworkLink();
const char* network_medium = network.mediumName();
if (network.isConnected()) {
strncpy(ip, network.localIP().toString().c_str(), sizeof(ip) - 1);
+1 -1
View File
@@ -6,7 +6,7 @@
#include <stdio.h>
#ifdef WITH_MQTT_BRIDGE
#include "AlertFaultPolicy.h"
#include "NetworkInterface.h"
#include "NetworkLink.h"
#endif
// Header layout for PAYLOAD_TYPE_GRP_TXT before encryption:
+13 -13
View File
@@ -15,7 +15,7 @@
#include "TxtDataHelpers.h"
#include "AlertReporter.h" // for alertReporterBannedChannelMatch[Hex]()
#include "MQTTObserverValidation.h" // pure input validators (host-testable)
#include "NetworkInterface.h"
#include "NetworkLink.h"
#include <Utils.h>
#include <climits>
#include <new>
@@ -416,9 +416,9 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
// runs on the Arduino loop task, shared with mesh/radio processing and the
// web config batch, so a synchronous wait of up to 30 s would stall the
// node. The sync runs in the background; verify with `get mqtt.ntp.diag`.
if (!activeNetworkInterface().isConnected()) {
if (!activeNetworkLink().isConnected()) {
snprintf(reply, 160, "OK - saved (%s not connected; NTP sync pending)",
activeNetworkInterface().mediumName());
activeNetworkLink().mediumName());
} else if (!_callbacks->isMqttBridgeRunning()) {
strcpy(reply, "OK - saved (MQTT bridge not running)");
} else if (_callbacks->syncMqttNtp()) {
@@ -460,8 +460,8 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
_mqtt_prefs.wifi_power_save = ps_value;
if (!persistObserverPrefs(reply)) return true;
#ifdef ESP_PLATFORM
if (strcmp(activeNetworkInterface().mediumName(), "wifi") == 0 &&
activeNetworkInterface().isConnected()) {
if (strcmp(activeNetworkLink().mediumName(), "wifi") == 0 &&
activeNetworkLink().isConnected()) {
wifi_ps_type_t ps_mode = (ps_value == 1) ? WIFI_PS_NONE :
(ps_value == 2) ? WIFI_PS_MAX_MODEM : WIFI_PS_MIN_MODEM;
esp_err_t ps_result = esp_wifi_set_ps(ps_mode);
@@ -962,9 +962,9 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
#ifdef ESP_PLATFORM
// Connectivity probe across all configured NTP servers; never updates the clock.
// Serial console (sender_timestamp == 0) gets a detailed table; LoRa gets a compact list.
if (!activeNetworkInterface().isConnected()) {
if (!activeNetworkLink().isConnected()) {
snprintf(reply, 160, "Error: %s not connected",
activeNetworkInterface().mediumName());
activeNetworkLink().mediumName());
} else if (!_callbacks->isMqttBridgeRunning()) {
strcpy(reply, "Error: MQTT bridge not running");
} else if (!_callbacks->runMqttNtpDiag(reply, 160, sender_timestamp == 0)) {
@@ -1039,10 +1039,10 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
strcpy(reply, _mqtt_prefs.wifi_password[0] ? "> ******** (serial only)" : "> (not set)");
}
} else if (strcmp(config, "link.diag") == 0) {
activeNetworkInterface().formatDiagnostics(reply, 160);
activeNetworkLink().formatDiagnostics(reply, 160);
} else if (memcmp(config, "link.status", 11) == 0 ||
memcmp(config, "wifi.status", 11) == 0) {
NetworkInterface& network = activeNetworkInterface();
NetworkLink& network = activeNetworkLink();
const bool wifi_alias = config[0] == 'w';
if (wifi_alias && strcmp(network.mediumName(), "wifi") != 0) {
snprintf(reply, 160, "> n/a (%s selected; use get link.status)",
@@ -1181,9 +1181,9 @@ bool CommonCLI::handleObserverCommand(uint32_t sender_timestamp, char* command,
#ifdef WITH_MQTT_BRIDGE
if (memcmp(command, "tls.bundletest ", 15) == 0) {
#ifdef ESP_PLATFORM
if (!activeNetworkInterface().isConnected()) {
if (!activeNetworkLink().isConnected()) {
snprintf(reply, 160, "ERR: %s not connected",
activeNetworkInterface().mediumName());
activeNetworkLink().mediumName());
} else {
size_t bundle_len = 0;
if (rootca_crt_bundle_start != nullptr &&
@@ -1228,9 +1228,9 @@ bool CommonCLI::handleObserverCommand(uint32_t sender_timestamp, char* command,
// ota check -> report available build, do not flash
// ota update -> download and flash, then reboot
#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_BASE)
if (!activeNetworkInterface().isConnected()) {
if (!activeNetworkLink().isConnected()) {
snprintf(reply, 160, "ERR: %s not connected",
activeNetworkInterface().mediumName());
activeNetworkLink().mediumName());
} else if (memcmp(command, "ota check", 9) == 0) {
// Check is synchronous so its result lands in this reply, and runs with the
// MQTT bridge UP: the slim per-variant manifest is tiny, so the fetch only
+8 -8
View File
@@ -1,7 +1,7 @@
#ifdef ESP_PLATFORM
#include "ESP32Board.h"
#include "NetworkInterface.h"
#include "NetworkLink.h"
#include <target.h>
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
@@ -46,7 +46,7 @@ void otaReleaseTransport() {
ota_server_running = false;
ota_started_at = 0;
if (ota_network_locked) {
activeNetworkInterface().unlockSwitching();
activeNetworkLink().unlockSwitching();
ota_network_locked = false;
}
HttpPort80Lease::release(HttpPort80Lease::Owner::Ota);
@@ -67,7 +67,7 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
}
inhibit_sleep = true; // prevent sleep during OTA
activeNetworkInterface().lockSwitching();
activeNetworkLink().lockSwitching();
ota_network_locked = true;
// If the device is already on its selected network, serve ElegantOTA on that
@@ -78,8 +78,8 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
// station IP can't be reached.
IPAddress ip;
if (NetworkPolicy::startOtaUsesSelectedNetwork(
force_ap, activeNetworkInterface().isConnected())) {
ip = activeNetworkInterface().localIP();
force_ap, activeNetworkLink().isConnected())) {
ip = activeNetworkLink().localIP();
} else {
ota_raised_ap = WiFi.softAP("MeshCore-OTA", NULL);
if (!ota_raised_ap) {
@@ -290,7 +290,7 @@ bool ESP32Board::otaFromManifest(const char* current_ver, bool dry_run, char rep
// mesh-receive call chain (it overflows the loopTask canary). Run the work in a
// dedicated 24 KB-stack task and block here until it finishes. The big stack is
// freed when the task exits; on a successful update the chip reboots inside it.
NetworkInterface& network = activeNetworkInterface();
NetworkLink& network = activeNetworkLink();
network.lockSwitching();
OtaTaskArgs args = { this, current_ver, dry_run, reply, false, false };
TaskHandle_t handle = nullptr;
@@ -312,9 +312,9 @@ bool ESP32Board::otaFromManifestImpl(const char* current_ver, bool dry_run, char
strcpy(reply, "ERR: OTA not configured (build via build.sh)");
return false;
#else
if (!activeNetworkInterface().isConnected()) {
if (!activeNetworkLink().isConnected()) {
snprintf(reply, 160, "ERR: %s not connected",
activeNetworkInterface().mediumName());
activeNetworkLink().mediumName());
return false;
}
@@ -1,4 +1,4 @@
#include "NetworkInterface.h"
#include "NetworkLink.h"
#if defined(ESP_PLATFORM)
@@ -21,9 +21,16 @@
extern void tcpipInit();
#endif
// Same "MQTT: " prefix as MQTT_DEBUG_PRINTLN so existing log greps keep matching.
#if defined(MQTT_DEBUG)
#define NETWORK_DEBUG_PRINTLN(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F "\n", ##__VA_ARGS__); } } while (0)
#else
#define NETWORK_DEBUG_PRINTLN(...) do {} while (0)
#endif
namespace {
class NetworkInterfaceBase : public NetworkInterface {
class NetworkLinkBase : public NetworkLink {
protected:
std::atomic<uint64_t> _outage_bits{AlertFaultPolicy::packOutageSnapshot({false, 0, 0})};
std::atomic<unsigned long> _connected_at{0};
@@ -75,7 +82,7 @@ class NetworkInterfaceBase : public NetworkInterface {
}
};
class WiFiNetworkInterface final : public NetworkInterfaceBase {
class WiFiNetworkLink final : public NetworkLinkBase {
bool _event_registered = false;
char _hostname[32] = {};
char _ssid[33] = {};
@@ -118,29 +125,36 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
_hostname[sizeof(_hostname) - 1] = '\0';
}
bool begin(const char* wifi_ssid, const char* wifi_password) override {
if (!configValid(wifi_ssid)) return false;
strncpy(_ssid, wifi_ssid, sizeof(_ssid) - 1);
void updateWifiCredentials(const char* wifi_ssid, const char* wifi_password) override {
strncpy(_ssid, wifi_ssid ? wifi_ssid : "", sizeof(_ssid) - 1);
_ssid[sizeof(_ssid) - 1] = '\0';
strncpy(_password, wifi_password ? wifi_password : "", sizeof(_password) - 1);
_password[sizeof(_password) - 1] = '\0';
}
bool begin(const char* wifi_ssid, const char* wifi_password) override {
if (!configValid(wifi_ssid)) return false;
updateWifiCredentials(wifi_ssid, wifi_password);
// Arduino-ESP32 applies this stored value when it creates the STA netif.
// It must be set before WiFi.mode()/begin() for the first DHCP exchange.
if (_hostname[0] != '\0') WiFi.setHostname(_hostname);
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.setAutoConnect(true);
if (!_event_registered) {
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
switch (event) {
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
NETWORK_DEBUG_PRINTLN("WiFi connected: %s",
IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
noteConnected(millis());
_reconnect_backoff_attempt = 0;
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
noteDisconnected(millis(), info.wifi_sta_disconnected.reason);
NETWORK_DEBUG_PRINTLN("WiFi disconnected: reason %d",
info.wifi_sta_disconnected.reason);
break;
default:
break;
@@ -195,7 +209,7 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
const bool transitioned = _last_connected;
if (transitioned) {
_connected_at.store(0, std::memory_order_relaxed);
} else if (snapshot.down && MQTTConnectionPolicy::wifiReconnectDue(
} else if (snapshot.down && _ssid[0] != '\0' && MQTTConnectionPolicy::wifiReconnectDue(
now_ms, snapshot.started_ms, (uint32_t)_last_reconnect_attempt,
_reconnect_backoff_attempt)) {
_last_reconnect_attempt = now_ms;
@@ -224,7 +238,7 @@ class WiFiNetworkInterface final : public NetworkInterfaceBase {
};
#if defined(NETWORK_PREFER_ETHERNET)
class EthernetNetworkInterface final : public NetworkInterfaceBase {
class EthernetNetworkLink final : public NetworkLinkBase {
public:
enum class EventState : uint8_t {
None,
@@ -402,9 +416,9 @@ class EthernetNetworkInterface final : public NetworkInterfaceBase {
}
};
class AutomaticNetworkInterface final : public NetworkInterface {
EthernetNetworkInterface _ethernet;
WiFiNetworkInterface _wifi;
class AutomaticNetworkLink final : public NetworkLink {
EthernetNetworkLink _ethernet;
WiFiNetworkLink _wifi;
std::atomic<NetworkMedium> _selected{NetworkMedium::None};
std::atomic<bool> _ethernet_started{false};
std::atomic<bool> _wifi_started{false};
@@ -417,16 +431,23 @@ class AutomaticNetworkInterface final : public NetworkInterface {
std::atomic<uint32_t> _ethernet_no_ip_since{0};
std::atomic<uint8_t> _switch_locks{0};
std::atomic<bool> _switch_in_progress{false};
std::atomic<bool> _ethernet_seen{false}; // held a lease at least once this boot
NetworkInterface& selectedInterface(NetworkMedium selected) {
return selected == NetworkMedium::Ethernet
? static_cast<NetworkInterface&>(_ethernet)
: static_cast<NetworkInterface&>(_wifi);
// Ethernet is the primary for alerts once it has carried traffic, or when it
// is the only configured medium; a Wi-Fi-only install keeps Wi-Fi alerts.
bool ethernetIsAlertPrimary() const {
return _ethernet_seen.load(std::memory_order_acquire) || !wifiConfigured();
}
const NetworkInterface& selectedInterface(NetworkMedium selected) const {
NetworkLink& selectedInterface(NetworkMedium selected) {
return selected == NetworkMedium::Ethernet
? static_cast<const NetworkInterface&>(_ethernet)
: static_cast<const NetworkInterface&>(_wifi);
? static_cast<NetworkLink&>(_ethernet)
: static_cast<NetworkLink&>(_wifi);
}
const NetworkLink& selectedInterface(NetworkMedium selected) const {
return selected == NetworkMedium::Ethernet
? static_cast<const NetworkLink&>(_ethernet)
: static_cast<const NetworkLink&>(_wifi);
}
bool wifiConfigured() const { return _wifi_ssid[0] != '\0'; }
@@ -444,14 +465,14 @@ class AutomaticNetworkInterface final : public NetworkInterface {
std::memory_order_release);
}
// Lock and mutation each publish one flag then read the other; seq_cst keeps
// both sides from missing each other.
bool beginUnlockedMutation() {
bool expected = false;
if (!_switch_in_progress.compare_exchange_strong(
expected, true, std::memory_order_acq_rel,
std::memory_order_relaxed)) {
if (!_switch_in_progress.compare_exchange_strong(expected, true)) {
return false;
}
if (_switch_locks.load(std::memory_order_acquire) != 0) {
if (_switch_locks.load() != 0) {
_switch_in_progress.store(false, std::memory_order_release);
return false;
}
@@ -532,6 +553,11 @@ class AutomaticNetworkInterface final : public NetworkInterface {
}
bool isAutomatic() const override { return true; }
void updateWifiCredentials(const char* wifi_ssid, const char* wifi_password) override {
rememberWifi(wifi_ssid, wifi_password);
_wifi.updateWifiCredentials(_wifi_ssid, _wifi_password);
}
void setHostname(const char* hostname) override {
// Whichever medium wins now or during a later failover presents the same
// stable DHCP identity to the LAN.
@@ -589,6 +615,7 @@ class AutomaticNetworkInterface final : public NetworkInterface {
delay(25);
}
if (_ethernet.isConnected()) _ethernet_seen.store(true, std::memory_order_release);
const NetworkMedium initial = NetworkPolicy::bootSelection(
_ethernet.isConnected(), wifiConfigured());
if (initial == NetworkMedium::Ethernet) {
@@ -606,7 +633,7 @@ class AutomaticNetworkInterface final : public NetworkInterface {
_switch_locks.load(std::memory_order_acquire) != 0;
const bool ethernet_stopped =
ethernet_started &&
_ethernet.eventState() == EthernetNetworkInterface::EventState::Stopped;
_ethernet.eventState() == EthernetNetworkLink::EventState::Stopped;
if (ethernet_stopped) {
_ethernet_started.store(false, std::memory_order_release);
ethernet_started = false;
@@ -615,11 +642,11 @@ class AutomaticNetworkInterface final : public NetworkInterface {
_last_ethernet_init_attempt.store(now_ms, std::memory_order_relaxed);
}
}
const EthernetNetworkInterface::EventState ethernet_event =
const EthernetNetworkLink::EventState ethernet_event =
_ethernet.eventState();
const bool ethernet_link_up =
ethernet_event == EthernetNetworkInterface::EventState::LinkUp ||
ethernet_event == EthernetNetworkInterface::EventState::GotIp;
ethernet_event == EthernetNetworkLink::EventState::LinkUp ||
ethernet_event == EthernetNetworkLink::EventState::GotIp;
if (ethernet_started && ethernet_link_up && !_ethernet.isConnected()) {
if (_ethernet_no_ip_since.load(std::memory_order_relaxed) == 0) {
uint32_t started_at = now_ms;
@@ -647,6 +674,7 @@ class AutomaticNetworkInterface final : public NetworkInterface {
const NetworkTransition ethernet_transition =
_ethernet.maintain(now_ms, wifi_power_save);
if (_ethernet.isConnected()) _ethernet_seen.store(true, std::memory_order_release);
const bool wifi_started = _wifi_started.load(std::memory_order_acquire);
const NetworkTransition wifi_transition = wifi_started
? _wifi.maintain(now_ms, wifi_power_save)
@@ -721,9 +749,8 @@ class AutomaticNetworkInterface final : public NetworkInterface {
void lockSwitching() override {
uint8_t value = _switch_locks.load(std::memory_order_relaxed);
while (value != UINT8_MAX && !_switch_locks.compare_exchange_weak(
value, static_cast<uint8_t>(value + 1),
std::memory_order_acq_rel, std::memory_order_relaxed)) {}
while (_switch_in_progress.load(std::memory_order_acquire)) delay(1);
value, static_cast<uint8_t>(value + 1))) {}
while (_switch_in_progress.load()) delay(1);
}
void unlockSwitching() override {
uint8_t value = _switch_locks.load(std::memory_order_relaxed);
@@ -819,21 +846,21 @@ class AutomaticNetworkInterface final : public NetworkInterface {
: selectedInterface(selected).outageSnapshot();
}
NetworkMedium alertMedium() const override {
return NetworkMedium::Ethernet;
return ethernetIsAlertPrimary() ? NetworkMedium::Ethernet : NetworkMedium::WiFi;
}
AlertFaultPolicy::OutageSnapshot alertOutageSnapshot() const override {
return _ethernet.outageSnapshot();
return ethernetIsAlertPrimary() ? _ethernet.outageSnapshot() : _wifi.outageSnapshot();
}
};
#endif
} // namespace
NetworkInterface& activeNetworkInterface() {
NetworkLink& activeNetworkLink() {
#if defined(NETWORK_PREFER_ETHERNET)
static AutomaticNetworkInterface network;
static AutomaticNetworkLink network;
#else
static WiFiNetworkInterface network;
static WiFiNetworkLink network;
#endif
return network;
}
@@ -20,9 +20,9 @@
* MQTT shutdown deliberately does not stop this interface because an OTA
* download runs after the broker clients have been released.
*/
class NetworkInterface {
class NetworkLink {
public:
virtual ~NetworkInterface() = default;
virtual ~NetworkLink() = default;
virtual const char* mediumName() const = 0;
virtual NetworkMedium medium() const = 0;
@@ -33,6 +33,11 @@ class NetworkInterface {
// retain it for any later fallback interface start.
virtual void setHostname(const char* hostname) = 0;
virtual bool begin(const char* wifi_ssid, const char* wifi_password) = 0;
// Latest stored credentials; the next reconnect attempt uses them without a reboot.
virtual void updateWifiCredentials(const char* wifi_ssid, const char* wifi_password) {
(void)wifi_ssid;
(void)wifi_password;
}
virtual NetworkTransition maintain(uint32_t now_ms, uint8_t wifi_power_save) = 0;
// Automatic selectors are boot-owned so first-run services can use the
@@ -71,6 +76,6 @@ class NetworkInterface {
};
/** Build-selected singleton. Wi-Fi is the compatibility default. */
NetworkInterface& activeNetworkInterface();
NetworkLink& activeNetworkLink();
#endif
+2 -2
View File
@@ -1,7 +1,7 @@
#ifdef WITH_SNMP
#include "SNMPAgent.h"
#include "NetworkInterface.h"
#include "NetworkLink.h"
#include <esp_heap_caps.h>
#include <climits>
@@ -79,7 +79,7 @@ void MeshSNMPAgent::loop() {
_psram_free = 0;
#endif
const int signal = activeNetworkInterface().rssi();
const int signal = activeNetworkLink().rssi();
_wifi_rssi = signal == INT_MIN ? -127 : signal;
_snmp.loop();
+1 -1
View File
@@ -14,7 +14,7 @@
// .2.x.0 = radio (packets, RSSI, SNR, noise floor, air time)
// .3.x.0 = mqtt (connected slots, queue depth, skipped publishes)
// .4.x.0 = memory (free heap, max alloc, internal free, PSRAM free)
// .5.x.0 = network (RSSI, or 0 when the selected medium has no RSSI)
// .5.x.0 = network (RSSI, or -127 when disconnected or the medium has no RSSI)
class MeshSNMPAgent {
public:
+6 -5
View File
@@ -101,7 +101,7 @@ void MQTTBridge::getEffectiveMqttOrigin(const NodePrefs* np, const MQTTPrefs* ob
}
static bool isNetworkConfigValid(const MQTTPrefs* obs) {
return obs && activeNetworkInterface().configValid(obs->wifi_ssid);
return obs && activeNetworkLink().configValid(obs->wifi_ssid);
}
#ifdef WITH_MQTT_BRIDGE
@@ -221,7 +221,7 @@ static void agentLogHeap(const char* location, const char* message, const char*
static MQTTBridge* s_mqtt_bridge_instance = nullptr;
unsigned long MQTTBridge::getWifiConnectedAtMillis() {
return activeNetworkInterface().connectedAtMillis();
return activeNetworkLink().connectedAtMillis();
}
#if defined(WITH_MQTT_NEIGHBORS)
@@ -413,10 +413,10 @@ int MQTTBridge::getMaxActiveSlots() {
}
uint8_t MQTTBridge::getLastWifiDisconnectReason() {
return activeNetworkInterface().lastDisconnectReason();
return activeNetworkLink().lastDisconnectReason();
}
unsigned long MQTTBridge::getLastWifiDisconnectTime() {
return activeNetworkInterface().lastDisconnectTime();
return activeNetworkLink().lastDisconnectTime();
}
unsigned long MQTTBridge::getSlotCurrentOutageStartMs(int slot_index) const {
@@ -624,7 +624,7 @@ static inline uint32_t mqttStopTimeoutForSlots(int slots) {
MQTTBridge::MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity)
: BridgeBase(prefs, mgr, rtc),
_obs(obs),
_network(&activeNetworkInterface()),
_network(&activeNetworkLink()),
_queue_count(0),
_last_status_publish(0), _last_status_retry(0), _status_interval(300000),
_ntp_client(_ntp_udp, effectiveNtpPrimary(obs), 0, 60000), _last_ntp_sync(0), _ntp_synced(false), _ntp_sync_pending(false), _slots_setup_done(false), _max_active_slots(RUNTIME_MQTT_SLOTS),
@@ -2740,6 +2740,7 @@ void MQTTBridge::checkConfigurationMismatch() {
bool MQTTBridge::handleNetworkConnection(unsigned long now) {
const NetworkMedium previous_medium = _network->medium();
_network->updateWifiCredentials(_obs->wifi_ssid, _obs->wifi_password);
const NetworkTransition transition =
_network->maintain((uint32_t)now, _obs->wifi_power_save);
const NetworkMedium selected_medium = _network->medium();
+2 -2
View File
@@ -2,7 +2,7 @@
#include "MeshCore.h"
#include "helpers/bridges/BridgeBase.h"
#include "helpers/NetworkInterface.h"
#include "helpers/NetworkLink.h"
#include <PsychicMqttClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
@@ -525,7 +525,7 @@ private:
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt.json.
// _prefs (held by BridgeBase) still provides upstream fields (freq/sf/node_name…).
MQTTPrefs* _obs = nullptr;
NetworkInterface* _network = nullptr;
NetworkLink* _network = nullptr;
public:
MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity);
+11 -11
View File
@@ -13,7 +13,7 @@
#include <SHA256.h>
#include <helpers/CommonCLI.h>
#include <helpers/NetworkInterface.h>
#include <helpers/NetworkLink.h>
#include <helpers/MQTTPacketFilter.h>
#include <helpers/MQTTPresets.h>
#include <helpers/WebConfigKeys.h>
@@ -232,7 +232,7 @@ bool WebConfigServer::getSetupInfo(char* ssid, size_t ssid_len, char* ip, size_t
}
if (ip && ip_len > 0) {
const IPAddress address = w->_mode == MODE_LAN
? activeNetworkInterface().localIP() : WiFi.softAPIP();
? activeNetworkLink().localIP() : WiFi.softAPIP();
snprintf(ip, ip_len, "%s", address.toString().c_str());
}
return true;
@@ -291,7 +291,7 @@ bool WebConfigServer::startSetupMode(char reply[]) {
_mode = MODE_SETUP;
_initial_setup = !mqttNetworkSetupComplete(_obs);
createServer();
activeNetworkInterface().lockSwitching();
activeNetworkLink().lockSwitching();
_network_locked = true;
_was_setup_ap = true;
_last_activity = millis();
@@ -311,10 +311,10 @@ bool WebConfigServer::startLanMode(IPAddress ip, bool initial_setup, char reply[
HttpPort80Lease::ownerName());
return false;
}
activeNetworkInterface().lockSwitching();
activeNetworkLink().lockSwitching();
_network_locked = true;
if (!activeNetworkInterface().isConnected() || ip == IPAddress()) {
activeNetworkInterface().unlockSwitching();
if (!activeNetworkLink().isConnected() || ip == IPAddress()) {
activeNetworkLink().unlockSwitching();
_network_locked = false;
HttpPort80Lease::release(HttpPort80Lease::Owner::WebConfig);
strcpy(reply, "Err: selected network not connected");
@@ -324,7 +324,7 @@ bool WebConfigServer::startLanMode(IPAddress ip, bool initial_setup, char reply[
uint8_t session_entropy[sizeof(_session_secret) + 6];
const size_t entropy_size = sizeof(_session_secret) + (_initial_setup ? 6 : 0);
if (!fillRandomBytes(session_entropy, entropy_size)) {
activeNetworkInterface().unlockSwitching();
activeNetworkLink().unlockSwitching();
_network_locked = false;
HttpPort80Lease::release(HttpPort80Lease::Owner::WebConfig);
strcpy(reply, "Err: secure random source unavailable");
@@ -445,7 +445,7 @@ void WebConfigServer::finalizeTeardown() {
_setup_code[0] = 0;
_setup_reminder_at = 0;
if (_network_locked) {
activeNetworkInterface().unlockSwitching();
activeNetworkLink().unlockSwitching();
_network_locked = false;
}
HttpPort80Lease::release(HttpPort80Lease::Owner::WebConfig);
@@ -474,7 +474,7 @@ void WebConfigServer::tick(uint32_t now) {
if (_mode == MODE_LAN && _initial_setup && _setup_reminder_at != 0 &&
(int32_t)(now - _setup_reminder_at) >= 0) {
Serial.printf("WC: Ethernet setup http://%s/ code %s\n",
activeNetworkInterface().localIP().toString().c_str(),
activeNetworkLink().localIP().toString().c_str(),
_setup_code);
_setup_reminder_at = now + 60000;
if (_setup_reminder_at == 0) _setup_reminder_at = 1;
@@ -581,8 +581,8 @@ void WebConfigServer::drainBatch(uint32_t now) {
return; // more commands next tick
}
}
if (_initial_setup && _admin_pwd_set && _batch_all_ok &&
(_mode == MODE_LAN || _obs->wifi_ssid[0] != '\0')) {
// Wi-Fi onboarding is recorded by the stored SSID; only Ethernet LAN setup needs the marker.
if (_initial_setup && _admin_pwd_set && _batch_all_ok && _mode == MODE_LAN) {
if (_cb->onInitialSetupComplete()) {
_initial_setup = false;
_setup_code[0] = 0;
@@ -485,6 +485,20 @@ TEST(MQTTPrefsSerializer, UnknownGroupsAreIgnoredSoAppendedKeysAreDowngradeSafe)
EXPECT_EQ(45, prefs.display_timeout_secs);
}
TEST(MQTTPrefsSerializer, UnknownKeysInsideAKnownGroupAreDowngradeSafe) {
// Firmware that predates wifi.setup_complete sees it as an unknown key in a
// known group; the rest of the group must still load.
MQTTPrefs prefs = defaults();
InputStream input(
"{version:1,wifi:{ssid:\"home\",future_key:1,power_save:2}}");
MQTTPrefsSerializer serializer(&prefs);
ASSERT_TRUE(serializer.loadSerial(input));
bool repaired = false;
ASSERT_TRUE(serializer.apply(&repaired));
EXPECT_STREQ("home", prefs.wifi_ssid);
EXPECT_EQ(2, prefs.wifi_power_save);
}
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();