feat: abstract MQTT network transport
This commit is contained in:
1 parent
f8fc35a11a
commit
03c0ad81fe
19 files changed
+731
-306
No files matched your search
@@ -1,7 +1,12 @@
|
||||
#include "MyMesh.h"
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <stdlib.h> // for qsort()
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#include <helpers/NetworkInterface.h>
|
||||
#if defined(ESP_PLATFORM) && !defined(NETWORK_USE_ETHERNET)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
#include <helpers/MQTTConnectionPolicy.h> // kSyncedClockEpoch
|
||||
#endif
|
||||
@@ -1218,11 +1223,13 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
#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) {
|
||||
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);
|
||||
@@ -1467,6 +1474,11 @@ 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");
|
||||
@@ -1490,6 +1502,7 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
_webconfig->startLanMode(reply); // reports "WiFi not connected" if down
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::stopWebConfig(char* reply) {
|
||||
@@ -1523,12 +1536,17 @@ void MyMesh::onConfigBatchEnd() {
|
||||
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
|
||||
char ip[20] = "";
|
||||
int wifi_rssi = 0;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
NetworkInterface& network = activeNetworkInterface();
|
||||
if (network.isConnected()) {
|
||||
strncpy(ip, network.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
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,"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "MyMesh.h"
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <helpers/RxReservePacketManager.h>
|
||||
#include <helpers/NetworkInterface.h>
|
||||
#if defined(ESP_PLATFORM) && !defined(NETWORK_USE_ETHERNET)
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
#include <helpers/MQTTConnectionPolicy.h> // kSyncedClockEpoch
|
||||
#endif
|
||||
@@ -1020,11 +1025,13 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
#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) {
|
||||
char wc_reply[160];
|
||||
startWebConfig(false, wc_reply);
|
||||
Serial.println(wc_reply);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1269,6 +1276,11 @@ 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");
|
||||
@@ -1292,6 +1304,7 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
_webconfig->startLanMode(reply); // reports "WiFi not connected" if down
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MyMesh::stopWebConfig(char* reply) {
|
||||
@@ -1325,12 +1338,17 @@ void MyMesh::onConfigBatchEnd() {
|
||||
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
|
||||
char ip[20] = "";
|
||||
int wifi_rssi = 0;
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
wifi_rssi = WiFi.RSSI();
|
||||
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
|
||||
NetworkInterface& network = activeNetworkInterface();
|
||||
if (network.isConnected()) {
|
||||
strncpy(ip, network.localIP().toString().c_str(), sizeof(ip) - 1);
|
||||
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,"
|
||||
|
||||
@@ -220,39 +220,58 @@ static inline void formatAge(uint32_t age_ms, char* out, size_t out_size) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline void formatWifiDown(char* out, size_t out_size, uint32_t duration_ms,
|
||||
uint8_t reason) {
|
||||
static inline void formatNetworkDown(char* out, size_t out_size, const char* medium,
|
||||
uint32_t duration_ms, uint8_t reason) {
|
||||
if (!out || out_size == 0) return;
|
||||
char age[16];
|
||||
formatAge(duration_ms, age, sizeof(age));
|
||||
if (reason != 0) {
|
||||
snprintf(out, out_size, "WiFi down %s (reason %u)", age, (unsigned)reason);
|
||||
snprintf(out, out_size, "%s down %s (reason %u)", medium, age, (unsigned)reason);
|
||||
} else {
|
||||
snprintf(out, out_size, "WiFi down %s", age);
|
||||
snprintf(out, out_size, "%s down %s", medium, age);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void formatNetworkRecovered(char* out, size_t out_size,
|
||||
const char* medium,
|
||||
uint32_t duration_ms) {
|
||||
if (!out || out_size == 0) return;
|
||||
char age[16];
|
||||
formatAge(duration_ms, age, sizeof(age));
|
||||
snprintf(out, out_size, "%s recovered after %s", medium, age);
|
||||
}
|
||||
|
||||
// Compatibility helpers retained for existing callers and native tests.
|
||||
static inline void formatWifiDown(char* out, size_t out_size,
|
||||
uint32_t duration_ms, uint8_t reason) {
|
||||
formatNetworkDown(out, out_size, "WiFi", duration_ms, reason);
|
||||
}
|
||||
|
||||
static inline void formatWifiRecovered(char* out, size_t out_size,
|
||||
uint32_t duration_ms) {
|
||||
if (!out || out_size == 0) return;
|
||||
char age[16];
|
||||
formatAge(duration_ms, age, sizeof(age));
|
||||
snprintf(out, out_size, "WiFi recovered after %s", age);
|
||||
formatNetworkRecovered(out, out_size, "WiFi", duration_ms);
|
||||
}
|
||||
|
||||
static inline bool formatNetworkAlert(char* out, size_t out_size,
|
||||
const char* medium, const TickResult& r,
|
||||
const OutageSnapshot& snap) {
|
||||
const char* label = (medium && *medium) ? medium : "Network";
|
||||
if (r.action == Action::FireDown) {
|
||||
formatNetworkDown(out, out_size, label, r.duration_ms, snap.reason);
|
||||
return true;
|
||||
}
|
||||
if (r.action == Action::FireRecovered) {
|
||||
formatNetworkRecovered(out, out_size, label, r.duration_ms);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Production formatting entry: the same (TickResult, OutageSnapshot) pair
|
||||
// AlertReporter feeds after tick(). Returns false when there is no message.
|
||||
static inline bool formatWifiAlert(char* out, size_t out_size, const TickResult& r,
|
||||
const OutageSnapshot& snap) {
|
||||
if (r.action == Action::FireDown) {
|
||||
formatWifiDown(out, out_size, r.duration_ms, snap.reason);
|
||||
return true;
|
||||
}
|
||||
if (r.action == Action::FireRecovered) {
|
||||
formatWifiRecovered(out, out_size, r.duration_ms);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return formatNetworkAlert(out, out_size, "WiFi", r, snap);
|
||||
}
|
||||
|
||||
static inline void formatMqttDown(char* out, size_t out_size, int slot_1based,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
#include "AlertFaultPolicy.h"
|
||||
#include "NetworkInterface.h"
|
||||
#endif
|
||||
|
||||
// Header layout for PAYLOAD_TYPE_GRP_TXT before encryption:
|
||||
@@ -217,13 +218,21 @@ void AlertReporter::onLoop(unsigned long now_ms) {
|
||||
min_interval_ms);
|
||||
if (r.action == AlertFaultPolicy::Action::FireDown) {
|
||||
char text[80];
|
||||
AlertFaultPolicy::formatWifiAlert(text, sizeof(text), r, snap);
|
||||
AlertFaultPolicy::formatNetworkAlert(
|
||||
text, sizeof(text),
|
||||
strcmp(activeNetworkInterface().mediumName(), "ethernet") == 0
|
||||
? "Ethernet" : "WiFi",
|
||||
r, snap);
|
||||
if (sendChannel(text)) {
|
||||
AlertFaultPolicy::commitDown(_wifi, now, snap.started_ms);
|
||||
}
|
||||
} else if (r.action == AlertFaultPolicy::Action::FireRecovered) {
|
||||
char text[80];
|
||||
AlertFaultPolicy::formatWifiAlert(text, sizeof(text), r, snap);
|
||||
AlertFaultPolicy::formatNetworkAlert(
|
||||
text, sizeof(text),
|
||||
strcmp(activeNetworkInterface().mediumName(), "ethernet") == 0
|
||||
? "Ethernet" : "WiFi",
|
||||
r, snap);
|
||||
sendChannel(text);
|
||||
AlertFaultPolicy::commitRecovered(_wifi);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "AlertReporter.h" // for alertReporterBannedChannelMatch[Hex]()
|
||||
#include "MQTTObserverValidation.h" // pure input validators (host-testable)
|
||||
#include "NetworkInterface.h"
|
||||
#include <Utils.h>
|
||||
#include <climits>
|
||||
#include <new>
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <WiFi.h>
|
||||
@@ -414,8 +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 (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "OK - saved (WiFi not connected; NTP sync pending)");
|
||||
if (!activeNetworkInterface().isConnected()) {
|
||||
snprintf(reply, 160, "OK - saved (%s not connected; NTP sync pending)",
|
||||
activeNetworkInterface().mediumName());
|
||||
} else if (!_callbacks->isMqttBridgeRunning()) {
|
||||
strcpy(reply, "OK - saved (MQTT bridge not running)");
|
||||
} else if (_callbacks->syncMqttNtp()) {
|
||||
@@ -457,7 +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 (WiFi.status() == WL_CONNECTED) {
|
||||
if (strcmp(activeNetworkInterface().mediumName(), "wifi") == 0 &&
|
||||
activeNetworkInterface().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);
|
||||
@@ -958,8 +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 (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "Error: WiFi not connected");
|
||||
if (!activeNetworkInterface().isConnected()) {
|
||||
snprintf(reply, 160, "Error: %s not connected",
|
||||
activeNetworkInterface().mediumName());
|
||||
} else if (!_callbacks->isMqttBridgeRunning()) {
|
||||
strcpy(reply, "Error: MQTT bridge not running");
|
||||
} else if (!_callbacks->runMqttNtpDiag(reply, 160, sender_timestamp == 0)) {
|
||||
@@ -1033,20 +1038,28 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
||||
} else {
|
||||
strcpy(reply, _mqtt_prefs.wifi_password[0] ? "> ******** (serial only)" : "> (not set)");
|
||||
}
|
||||
} else if (memcmp(config, "wifi.status", 11) == 0) {
|
||||
wl_status_t status = WiFi.status();
|
||||
const char* status_str;
|
||||
switch (status) {
|
||||
case WL_CONNECTED: status_str = "connected"; break;
|
||||
case WL_NO_SSID_AVAIL: status_str = "no_ssid"; break;
|
||||
case WL_CONNECT_FAILED: status_str = "connect_failed"; break;
|
||||
case WL_CONNECTION_LOST: status_str = "connection_lost"; break;
|
||||
case WL_DISCONNECTED: status_str = "disconnected"; break;
|
||||
case 255: status_str = "not_started"; break;
|
||||
default: status_str = "unknown"; break;
|
||||
} else if (memcmp(config, "link.status", 11) == 0 ||
|
||||
memcmp(config, "wifi.status", 11) == 0) {
|
||||
NetworkInterface& network = activeNetworkInterface();
|
||||
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)",
|
||||
network.mediumName());
|
||||
return true;
|
||||
}
|
||||
if (status == WL_CONNECTED) {
|
||||
sprintf(reply, "> %s, IP: %s, RSSI: %d dBm", status_str, WiFi.localIP().toString().c_str(), WiFi.RSSI());
|
||||
const bool connected = network.isConnected();
|
||||
if (connected) {
|
||||
const int signal = network.rssi();
|
||||
if (wifi_alias) {
|
||||
snprintf(reply, 160, "> %s, IP: %s, RSSI: %d dBm",
|
||||
network.statusName(), network.localIP().toString().c_str(), signal);
|
||||
} else if (signal == INT_MIN) {
|
||||
snprintf(reply, 160, "> %s: connected, IP: %s", network.mediumName(),
|
||||
network.localIP().toString().c_str());
|
||||
} else {
|
||||
snprintf(reply, 160, "> %s: connected, IP: %s, RSSI: %d dBm",
|
||||
network.mediumName(), network.localIP().toString().c_str(), signal);
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
unsigned long connect_at = MQTTBridge::getWifiConnectedAtMillis();
|
||||
if (connect_at != 0) {
|
||||
@@ -1074,19 +1087,35 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
||||
#endif
|
||||
} else {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
uint8_t reason = MQTTBridge::getLastWifiDisconnectReason();
|
||||
uint8_t reason = network.lastDisconnectReason();
|
||||
if (reason != 0) {
|
||||
const char* desc = MQTTBridge::wifiReasonStr(reason);
|
||||
if (desc) {
|
||||
sprintf(reply, "> %s: %s (reason: %d)", status_str, desc, reason);
|
||||
if (wifi_alias) {
|
||||
sprintf(reply, "> %s: %s (reason: %d)", network.statusName(), desc, reason);
|
||||
} else {
|
||||
sprintf(reply, "> %s: %s (reason: %d)", network.mediumName(), desc, reason);
|
||||
}
|
||||
} else {
|
||||
sprintf(reply, "> %s: reason %d", status_str, reason);
|
||||
if (wifi_alias) {
|
||||
sprintf(reply, "> %s: reason %d", network.statusName(), reason);
|
||||
} else {
|
||||
sprintf(reply, "> %s: reason %d", network.mediumName(), reason);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sprintf(reply, "> %s (code: %d)", status_str, status);
|
||||
if (wifi_alias) {
|
||||
sprintf(reply, "> %s (code: %d)", network.statusName(), network.statusCode());
|
||||
} else {
|
||||
sprintf(reply, "> %s: %s", network.mediumName(), network.statusName());
|
||||
}
|
||||
}
|
||||
#else
|
||||
sprintf(reply, "> %s (code: %d)", status_str, status);
|
||||
if (wifi_alias) {
|
||||
sprintf(reply, "> %s (code: %d)", network.statusName(), network.statusCode());
|
||||
} else {
|
||||
sprintf(reply, "> %s: %s", network.mediumName(), network.statusName());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (memcmp(config, "wifi.powersave", 14) == 0) {
|
||||
@@ -1150,8 +1179,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 (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "ERR: WiFi not connected");
|
||||
if (!activeNetworkInterface().isConnected()) {
|
||||
snprintf(reply, 160, "ERR: %s not connected",
|
||||
activeNetworkInterface().mediumName());
|
||||
} else {
|
||||
size_t bundle_len = 0;
|
||||
if (rootca_crt_bundle_start != nullptr &&
|
||||
@@ -1196,8 +1226,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 (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "ERR: WiFi not connected");
|
||||
if (!activeNetworkInterface().isConnected()) {
|
||||
snprintf(reply, 160, "ERR: %s not connected",
|
||||
activeNetworkInterface().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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "ESP32Board.h"
|
||||
#include "NetworkInterface.h"
|
||||
#include <target.h>
|
||||
|
||||
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
|
||||
@@ -14,15 +15,16 @@
|
||||
bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
inhibit_sleep = true; // prevent sleep during OTA
|
||||
|
||||
// If the device is already on a WiFi network (e.g. an observer joined in STA
|
||||
// mode), serve ElegantOTA on the station IP so it's reachable from the LAN
|
||||
// without joining a separate AP. Otherwise raise the MeshCore-OTA SoftAP.
|
||||
// If the device is already on its selected network, serve ElegantOTA on that
|
||||
// address so it is reachable without joining a separate AP. Otherwise raise
|
||||
// the MeshCore-OTA SoftAP.
|
||||
// force_ap ("start ota ap") always raises the SoftAP, so the OTA UI stays
|
||||
// reachable even when the joined network applies client isolation and the
|
||||
// station IP can't be reached.
|
||||
IPAddress ip;
|
||||
if (!force_ap && WiFi.status() == WL_CONNECTED) {
|
||||
ip = WiFi.localIP();
|
||||
if (NetworkPolicy::startOtaUsesSelectedNetwork(
|
||||
force_ap, activeNetworkInterface().isConnected())) {
|
||||
ip = activeNetworkInterface().localIP();
|
||||
} else {
|
||||
WiFi.softAP("MeshCore-OTA", NULL);
|
||||
ip = WiFi.softAPIP();
|
||||
@@ -200,8 +202,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 (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "ERR: WiFi not connected");
|
||||
if (!activeNetworkInterface().isConnected()) {
|
||||
snprintf(reply, 160, "ERR: %s not connected",
|
||||
activeNetworkInterface().mediumName());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "NetworkInterface.h"
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
|
||||
#include "MQTTConnectionPolicy.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#if defined(NETWORK_USE_ETHERNET)
|
||||
#include "ethernet/ch390/CH390Config.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
class NetworkInterfaceBase : public NetworkInterface {
|
||||
protected:
|
||||
std::atomic<uint64_t> _outage_bits{AlertFaultPolicy::packOutageSnapshot({false, 0, 0})};
|
||||
std::atomic<unsigned long> _connected_at{0};
|
||||
std::atomic<unsigned long> _last_disconnect_time{0};
|
||||
std::atomic<uint8_t> _last_disconnect_reason{0};
|
||||
bool _status_initialized = false;
|
||||
bool _last_connected = false;
|
||||
unsigned long _last_status_check = 0;
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot outage() const {
|
||||
return AlertFaultPolicy::unpackOutageSnapshot(
|
||||
_outage_bits.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
void setOutage(AlertFaultPolicy::OutageSnapshot snapshot) {
|
||||
_outage_bits.store(AlertFaultPolicy::packOutageSnapshot(snapshot),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
void noteConnected(unsigned long now_ms) {
|
||||
if (_connected_at.load(std::memory_order_relaxed) == 0) {
|
||||
_connected_at.store(now_ms, std::memory_order_relaxed);
|
||||
}
|
||||
setOutage(AlertFaultPolicy::applyWifiGotIp(outage()));
|
||||
}
|
||||
|
||||
void noteDisconnected(unsigned long now_ms, uint8_t reason) {
|
||||
_last_disconnect_reason.store(reason, std::memory_order_relaxed);
|
||||
_last_disconnect_time.store(now_ms, std::memory_order_relaxed);
|
||||
setOutage(AlertFaultPolicy::applyWifiDisconnectEvent(
|
||||
(uint32_t)now_ms, reason, outage()));
|
||||
}
|
||||
|
||||
public:
|
||||
unsigned long connectedAtMillis() const override {
|
||||
return _connected_at.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
uint8_t lastDisconnectReason() const override {
|
||||
return _last_disconnect_reason.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
unsigned long lastDisconnectTime() const override {
|
||||
return _last_disconnect_time.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot outageSnapshot() const override {
|
||||
return outage();
|
||||
}
|
||||
};
|
||||
|
||||
class WiFiNetworkInterface final : public NetworkInterfaceBase {
|
||||
bool _event_registered = false;
|
||||
char _ssid[33] = {};
|
||||
char _password[65] = {};
|
||||
unsigned long _last_reconnect_attempt = 0;
|
||||
uint8_t _reconnect_backoff_attempt = 0;
|
||||
|
||||
void applyPowerPrefs(uint8_t wifi_power_save) {
|
||||
wifi_ps_type_t ps_mode = wifi_power_save == 2 ? WIFI_PS_MAX_MODEM : WIFI_PS_NONE;
|
||||
esp_wifi_set_ps(ps_mode);
|
||||
#ifdef MQTT_WIFI_TX_POWER
|
||||
WiFi.setTxPower(MQTT_WIFI_TX_POWER);
|
||||
#else
|
||||
WiFi.setTxPower(WIFI_POWER_11dBm);
|
||||
#endif
|
||||
}
|
||||
|
||||
public:
|
||||
const char* mediumName() const override { return "wifi"; }
|
||||
const char* statusName() const override {
|
||||
switch (WiFi.status()) {
|
||||
case WL_CONNECTED: return "connected";
|
||||
case WL_NO_SSID_AVAIL: return "no_ssid";
|
||||
case WL_CONNECT_FAILED: return "connect_failed";
|
||||
case WL_CONNECTION_LOST: return "connection_lost";
|
||||
case WL_DISCONNECTED: return "disconnected";
|
||||
case 255: return "not_started";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
int statusCode() const override { return (int)WiFi.status(); }
|
||||
|
||||
bool configValid(const char* wifi_ssid) const override {
|
||||
return wifi_ssid && wifi_ssid[0] != '\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);
|
||||
_ssid[sizeof(_ssid) - 1] = '\0';
|
||||
strncpy(_password, wifi_password ? wifi_password : "", sizeof(_password) - 1);
|
||||
_password[sizeof(_password) - 1] = '\0';
|
||||
|
||||
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:
|
||||
noteConnected(millis());
|
||||
_reconnect_backoff_attempt = 0;
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
noteDisconnected(millis(), info.wifi_sta_disconnected.reason);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
_event_registered = true;
|
||||
}
|
||||
|
||||
// Preserve the existing restart behavior: MQTT stop leaves the station up,
|
||||
// and begin() must not force a disconnect that races the first DNS lookup.
|
||||
if (!isConnected()) {
|
||||
WiFi.begin(_ssid, _password);
|
||||
} else {
|
||||
noteConnected(millis());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
NetworkTransition maintain(uint32_t now_ms, uint8_t wifi_power_save) override {
|
||||
const bool connected = isConnected();
|
||||
if (connected && connectedAtMillis() == 0) noteConnected(now_ms);
|
||||
|
||||
if (!_status_initialized) {
|
||||
_last_connected = connected;
|
||||
_status_initialized = true;
|
||||
setOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, connected, outage(), false));
|
||||
}
|
||||
|
||||
if ((uint32_t)(now_ms - _last_status_check) <= 10000) {
|
||||
if (connected && outage().down) noteConnected(now_ms);
|
||||
return NetworkTransition::None;
|
||||
}
|
||||
_last_status_check = now_ms;
|
||||
|
||||
if (connected) {
|
||||
const bool transitioned = !_last_connected;
|
||||
if (transitioned) {
|
||||
setOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, true, outage(), true));
|
||||
_connected_at.store(now_ms, std::memory_order_relaxed);
|
||||
_reconnect_backoff_attempt = 0;
|
||||
applyPowerPrefs(wifi_power_save);
|
||||
}
|
||||
_last_connected = true;
|
||||
return transitioned ? NetworkTransition::Up : NetworkTransition::None;
|
||||
}
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot snapshot = AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, false, outage(), true);
|
||||
setOutage(snapshot);
|
||||
const bool transitioned = _last_connected;
|
||||
if (transitioned) {
|
||||
_connected_at.store(0, std::memory_order_relaxed);
|
||||
} else if (snapshot.down && MQTTConnectionPolicy::wifiReconnectDue(
|
||||
now_ms, snapshot.started_ms, (uint32_t)_last_reconnect_attempt,
|
||||
_reconnect_backoff_attempt)) {
|
||||
_last_reconnect_attempt = now_ms;
|
||||
_reconnect_backoff_attempt =
|
||||
MQTTConnectionPolicy::nextWifiBackoffAttempt(_reconnect_backoff_attempt);
|
||||
WiFi.disconnect();
|
||||
WiFi.begin(_ssid, _password);
|
||||
}
|
||||
_last_connected = false;
|
||||
return transitioned ? NetworkTransition::Down : NetworkTransition::None;
|
||||
}
|
||||
|
||||
bool isConnected() const override { return WiFi.status() == WL_CONNECTED; }
|
||||
IPAddress localIP() const override { return WiFi.localIP(); }
|
||||
int rssi() const override { return isConnected() ? WiFi.RSSI() : INT_MIN; }
|
||||
bool resolveHost(const char* hostname, IPAddress& address) const override {
|
||||
return WiFi.hostByName(hostname, address);
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(NETWORK_USE_ETHERNET)
|
||||
class EthernetNetworkInterface final : public NetworkInterfaceBase {
|
||||
bool _started = false;
|
||||
bool _event_registered = false;
|
||||
|
||||
public:
|
||||
const char* mediumName() const override { return "ethernet"; }
|
||||
const char* statusName() const override {
|
||||
return isConnected() ? "connected" : "disconnected";
|
||||
}
|
||||
int statusCode() const override { return isConnected() ? 1 : 0; }
|
||||
bool configValid(const char*) const override { return true; }
|
||||
|
||||
bool begin(const char*, const char*) override {
|
||||
if (_started) return true;
|
||||
if (!_event_registered) {
|
||||
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t) {
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
noteConnected(millis());
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||
// Ethernet has no 802.11 reason code; zero means unavailable.
|
||||
noteDisconnected(millis(), 0);
|
||||
_connected_at.store(0, std::memory_order_relaxed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
_event_registered = true;
|
||||
}
|
||||
_started = beginConfiguredCH390();
|
||||
if (_started && isConnected()) noteConnected(millis());
|
||||
return _started;
|
||||
}
|
||||
|
||||
NetworkTransition maintain(uint32_t now_ms, uint8_t) override {
|
||||
const bool connected = isConnected();
|
||||
if (!_status_initialized) {
|
||||
_last_connected = connected;
|
||||
_status_initialized = true;
|
||||
setOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, connected, outage(), false));
|
||||
if (connected) noteConnected(now_ms);
|
||||
return NetworkTransition::None;
|
||||
}
|
||||
|
||||
if (connected == _last_connected) {
|
||||
if (connected && outage().down) noteConnected(now_ms);
|
||||
return NetworkTransition::None;
|
||||
}
|
||||
|
||||
_last_connected = connected;
|
||||
if (connected) {
|
||||
_connected_at.store(now_ms, std::memory_order_relaxed);
|
||||
setOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, true, outage(), true));
|
||||
return NetworkTransition::Up;
|
||||
}
|
||||
|
||||
const bool outage_was_down = outage().down;
|
||||
_connected_at.store(0, std::memory_order_relaxed);
|
||||
AlertFaultPolicy::OutageSnapshot snapshot = AlertFaultPolicy::applyWifiStatus(
|
||||
now_ms, false, outage(), true);
|
||||
setOutage(snapshot);
|
||||
if (!outage_was_down) {
|
||||
_last_disconnect_time.store(now_ms, std::memory_order_relaxed);
|
||||
}
|
||||
return NetworkTransition::Down;
|
||||
}
|
||||
|
||||
bool isConnected() const override { return _started && CH390.isConnected(); }
|
||||
IPAddress localIP() const override { return CH390.localIP(); }
|
||||
int rssi() const override { return INT_MIN; }
|
||||
bool resolveHost(const char* hostname, IPAddress& address) const override {
|
||||
// Arduino's hostByName is a thin wrapper over the process-wide lwIP resolver;
|
||||
// DNS follows the selected esp_netif even though this entry point is named WiFi.
|
||||
return WiFi.hostByName(hostname, address);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkInterface& activeNetworkInterface() {
|
||||
#if defined(NETWORK_USE_ETHERNET)
|
||||
static EthernetNetworkInterface network;
|
||||
#else
|
||||
static WiFiNetworkInterface network;
|
||||
#endif
|
||||
return network;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "NetworkPolicy.h"
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <IPAddress.h>
|
||||
#include "AlertFaultPolicy.h"
|
||||
|
||||
/**
|
||||
* Physical network selected for IP-based services.
|
||||
*
|
||||
* The interface owns link bring-up and medium-specific maintenance. MQTT, NTP,
|
||||
* OTA, and other socket users only consume connectivity and addressing. Normal
|
||||
* MQTT shutdown deliberately does not stop this interface because an OTA
|
||||
* download runs after the broker clients have been released.
|
||||
*/
|
||||
class NetworkInterface {
|
||||
public:
|
||||
virtual ~NetworkInterface() = default;
|
||||
|
||||
virtual const char* mediumName() const = 0;
|
||||
virtual const char* statusName() const = 0;
|
||||
virtual int statusCode() const = 0;
|
||||
virtual bool configValid(const char* wifi_ssid) const = 0;
|
||||
virtual bool begin(const char* wifi_ssid, const char* wifi_password) = 0;
|
||||
virtual NetworkTransition maintain(uint32_t now_ms, uint8_t wifi_power_save) = 0;
|
||||
|
||||
virtual bool isConnected() const = 0;
|
||||
virtual IPAddress localIP() const = 0;
|
||||
virtual int rssi() const = 0; // INT_MIN when the selected medium has no RSSI.
|
||||
virtual bool resolveHost(const char* hostname, IPAddress& address) const = 0;
|
||||
|
||||
virtual unsigned long connectedAtMillis() const = 0;
|
||||
virtual uint8_t lastDisconnectReason() const = 0;
|
||||
virtual unsigned long lastDisconnectTime() const = 0;
|
||||
virtual AlertFaultPolicy::OutageSnapshot outageSnapshot() const = 0;
|
||||
};
|
||||
|
||||
/** Build-selected singleton. Wi-Fi is the compatibility default. */
|
||||
NetworkInterface& activeNetworkInterface();
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
enum class NetworkTransition : uint8_t {
|
||||
None,
|
||||
Up,
|
||||
Down,
|
||||
};
|
||||
|
||||
namespace NetworkPolicy {
|
||||
|
||||
struct MQTTTransitionActions {
|
||||
bool disconnect_slots;
|
||||
bool retry_disconnected_slots_now;
|
||||
};
|
||||
|
||||
static constexpr MQTTTransitionActions mqttActions(NetworkTransition transition) {
|
||||
return {
|
||||
transition == NetworkTransition::Down,
|
||||
transition == NetworkTransition::Up,
|
||||
};
|
||||
}
|
||||
|
||||
// `start ota` uses the selected LAN only when reachable and not explicitly
|
||||
// forced to SoftAP. Manifest OTA has no fallback and checks connectivity itself.
|
||||
static constexpr bool startOtaUsesSelectedNetwork(bool force_ap,
|
||||
bool network_connected) {
|
||||
return !force_ap && network_connected;
|
||||
}
|
||||
|
||||
} // namespace NetworkPolicy
|
||||
@@ -1,7 +1,9 @@
|
||||
#ifdef WITH_SNMP
|
||||
|
||||
#include "SNMPAgent.h"
|
||||
#include "NetworkInterface.h"
|
||||
#include <esp_heap_caps.h>
|
||||
#include <climits>
|
||||
|
||||
#define SNMP_PORT 161
|
||||
|
||||
@@ -67,7 +69,7 @@ void MeshSNMPAgent::begin(const char* community) {
|
||||
void MeshSNMPAgent::loop() {
|
||||
if (!_running) return;
|
||||
|
||||
// Update memory and network stats locally (we're on Core 0 with WiFi)
|
||||
// Update memory and selected-network stats locally on Core 0.
|
||||
_free_heap = (int)ESP.getFreeHeap();
|
||||
_max_alloc = (int)ESP.getMaxAllocHeap();
|
||||
_internal_free = (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
@@ -77,9 +79,8 @@ void MeshSNMPAgent::loop() {
|
||||
_psram_free = 0;
|
||||
#endif
|
||||
|
||||
if (WiFi.isConnected()) {
|
||||
_wifi_rssi = (int)WiFi.RSSI();
|
||||
}
|
||||
const int signal = activeNetworkInterface().rssi();
|
||||
_wifi_rssi = signal == INT_MIN ? 0 : signal;
|
||||
|
||||
_snmp.loop();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#ifdef WITH_SNMP
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <SNMP_Agent.h>
|
||||
|
||||
@@ -15,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 (WiFi RSSI)
|
||||
// .5.x.0 = network (RSSI, or 0 when the selected medium has no RSSI)
|
||||
|
||||
class MeshSNMPAgent {
|
||||
public:
|
||||
|
||||
@@ -100,16 +100,8 @@ void MQTTBridge::getEffectiveMqttOrigin(const NodePrefs* np, const MQTTPrefs* ob
|
||||
applyEffectiveOrigin(np, obs, buf, buf_size);
|
||||
}
|
||||
|
||||
// Helper function to check if WiFi credentials are valid
|
||||
static bool isWiFiConfigValid(const MQTTPrefs* obs) {
|
||||
// Check if WiFi SSID is configured (not empty)
|
||||
if (!obs || strlen(obs->wifi_ssid) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// WiFi password can be empty for open networks, so we don't check it
|
||||
|
||||
return true;
|
||||
static bool isNetworkConfigValid(const MQTTPrefs* obs) {
|
||||
return obs && activeNetworkInterface().configValid(obs->wifi_ssid);
|
||||
}
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -122,7 +114,7 @@ static bool customEndpointComplete(const char* host, uint16_t port) {
|
||||
}
|
||||
|
||||
bool MQTTBridge::isConfigValid(const MQTTPrefs* obs) {
|
||||
if (!obs || !isWiFiConfigValid(obs)) return false;
|
||||
if (!obs || !isNetworkConfigValid(obs)) return false;
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
const char* preset_name = obs->mqtt_slot_preset[i];
|
||||
if (preset_name[0] == '\0' || strcmp(preset_name, MQTT_PRESET_NONE) == 0) continue;
|
||||
@@ -210,13 +202,6 @@ void* MQTTBridge::JsonScratchAllocator::reallocate(void* ptr, size_t new_size) {
|
||||
return psram_realloc(ptr, new_size);
|
||||
}
|
||||
|
||||
// Time (millis()) when WiFi was last seen connected; 0 when disconnected. Used for get wifi.status uptime.
|
||||
static unsigned long s_wifi_connected_at = 0;
|
||||
|
||||
// Last WiFi disconnect reason (from ESP-IDF event). Used for get wifi.status diagnostics.
|
||||
static uint8_t s_wifi_disconnect_reason = 0;
|
||||
static unsigned long s_wifi_disconnect_time = 0;
|
||||
|
||||
#ifdef MQTT_MEMORY_DEBUG
|
||||
// #region agent log
|
||||
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
|
||||
@@ -236,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 s_wifi_connected_at;
|
||||
return activeNetworkInterface().connectedAtMillis();
|
||||
}
|
||||
|
||||
#if defined(WITH_MQTT_NEIGHBORS)
|
||||
@@ -427,8 +412,12 @@ int MQTTBridge::getMaxActiveSlots() {
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t MQTTBridge::getLastWifiDisconnectReason() { return s_wifi_disconnect_reason; }
|
||||
unsigned long MQTTBridge::getLastWifiDisconnectTime() { return s_wifi_disconnect_time; }
|
||||
uint8_t MQTTBridge::getLastWifiDisconnectReason() {
|
||||
return activeNetworkInterface().lastDisconnectReason();
|
||||
}
|
||||
unsigned long MQTTBridge::getLastWifiDisconnectTime() {
|
||||
return activeNetworkInterface().lastDisconnectTime();
|
||||
}
|
||||
|
||||
unsigned long MQTTBridge::getSlotCurrentOutageStartMs(int slot_index) const {
|
||||
if (slot_index < 0 || slot_index >= RUNTIME_MQTT_SLOTS) return 0;
|
||||
@@ -635,6 +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()),
|
||||
_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),
|
||||
@@ -661,8 +651,6 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mg
|
||||
#ifdef WITH_SNMP
|
||||
_snmp_agent(nullptr),
|
||||
#endif
|
||||
_last_wifi_check(0), _last_wifi_status(WL_DISCONNECTED), _wifi_status_initialized(false),
|
||||
_wifi_outage_bits{0}, _last_wifi_reconnect_attempt(0), _wifi_reconnect_backoff_attempt(0),
|
||||
_last_slot_reconnect_ms(0)
|
||||
#ifdef ESP_PLATFORM
|
||||
, _packet_queue_handle(nullptr), _mqtt_task_handle(nullptr),
|
||||
@@ -857,9 +845,10 @@ void MQTTBridge::begin() {
|
||||
_max_active_slots = getMaxActiveSlots();
|
||||
MQTT_DEBUG_PRINTLN("Max active slots: %d", _max_active_slots);
|
||||
|
||||
// Check if WiFi credentials are configured first
|
||||
if (!isWiFiConfigValid(_obs)) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - WiFi credentials not configured");
|
||||
// Ethernet needs no credentials; Wi-Fi preserves the existing SSID gate.
|
||||
if (!isNetworkConfigValid(_obs)) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - %s is not configured",
|
||||
_network->mediumName());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1043,11 +1032,8 @@ void MQTTBridge::begin() {
|
||||
|
||||
MQTT_DEBUG_PRINTLN("MQTT task created on Core %d", MQTT_TASK_CORE);
|
||||
#else
|
||||
// Non-ESP32: Initialize WiFi directly (no task)
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.setAutoConnect(true);
|
||||
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
|
||||
// Non-ESP32: initialize the selected network directly (no task).
|
||||
_network->begin(_obs->wifi_ssid, _obs->wifi_password);
|
||||
|
||||
// NOTE: Slot setup deferred until after NTP sync in loop()
|
||||
#endif
|
||||
@@ -1237,75 +1223,31 @@ void MQTTBridge::mqttTask(void* parameter) {
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void MQTTBridge::initializeWiFiInTask() {
|
||||
MQTT_DEBUG_PRINTLN("Initializing WiFi in MQTT task...");
|
||||
void MQTTBridge::initializeNetworkInTask() {
|
||||
MQTT_DEBUG_PRINTLN("Initializing %s network in MQTT task...", _network->mediumName());
|
||||
|
||||
// Initialize WiFi
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
// Enable automatic reconnection - ESP32 will handle reconnection automatically
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.setAutoConnect(true);
|
||||
|
||||
// Set up WiFi event handlers for better diagnostics and immediate disconnection
|
||||
// detection. Register ONCE — the bridge is reused across restarts (e.g. stopped
|
||||
// for `ota check`/`ota update`, or `set mqtt…` reconfigure) and WiFi.onEvent()
|
||||
// never removes prior callbacks, so re-registering leaks handlers and duplicates
|
||||
// every log line.
|
||||
if (!_wifi_event_registered) {
|
||||
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch(event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiGotIp(wifiOutage()));
|
||||
_wifi_reconnect_backoff_attempt = 0;
|
||||
// Set flag to trigger NTP sync from loop() instead of doing it here
|
||||
if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: {
|
||||
const uint8_t reason = info.wifi_sta_disconnected.reason;
|
||||
const unsigned long t = millis();
|
||||
s_wifi_disconnect_reason = reason;
|
||||
s_wifi_disconnect_time = t;
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiDisconnectEvent(
|
||||
(uint32_t)t, reason, wifiOutage()));
|
||||
MQTT_DEBUG_PRINTLN("WiFi disconnected: reason %d", s_wifi_disconnect_reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
_wifi_event_registered = true;
|
||||
}
|
||||
|
||||
// Only (re)start the WiFi association if it isn't already up. end() leaves the
|
||||
// STA link connected, so on a restart (e.g. after `ota check`) calling
|
||||
// WiFi.begin() again forces a needless disconnect/reconnect — which also races
|
||||
// the MQTT task's first DNS lookup (getaddrinfo fails until WiFi/DNS recovers).
|
||||
// When already connected, the deferred slot setup still fires in mqttTaskLoop()
|
||||
// because _ntp_synced persists across end() (only _slots_setup_done is reset).
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
|
||||
} else if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true; // already connected but never synced — kick NTP now
|
||||
// begin() is idempotent and deliberately leaves an already-up link alone.
|
||||
// MQTT end()/begin() cycles therefore keep the transport alive for OTA and do
|
||||
// not race the first DNS lookup after a bridge restart.
|
||||
if (!_network->begin(_obs->wifi_ssid, _obs->wifi_password)) {
|
||||
MQTT_DEBUG_PRINTLN("%s network initialization failed", _network->mediumName());
|
||||
} else if (_network->isConnected() && !_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
|
||||
// NOTE: Slot setup is deferred until after NTP sync in mqttTaskLoop().
|
||||
// JWT-auth slots need valid timestamps for token creation, and connecting
|
||||
// before NTP sync just wastes heap on TLS handshakes that will be rejected.
|
||||
|
||||
MQTT_DEBUG_PRINTLN("WiFi initialization started in task");
|
||||
MQTT_DEBUG_PRINTLN("%s network initialization started in task", _network->mediumName());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mqttTaskLoop() - main loop running on Core 0
|
||||
// ---------------------------------------------------------------------------
|
||||
void MQTTBridge::mqttTaskLoop() {
|
||||
// Initialize WiFi first
|
||||
initializeWiFiInTask();
|
||||
// Initialize the selected physical network first.
|
||||
initializeNetworkInTask();
|
||||
|
||||
// Wait a bit for WiFi to start connecting
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
@@ -1362,9 +1304,9 @@ void MQTTBridge::mqttTaskLoop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool wifi_just_connected = handleWiFiConnection(now);
|
||||
if (wifi_just_connected) {
|
||||
// WiFi recovered — reset last_reconnect_attempt for disconnected slots so they
|
||||
bool network_just_connected = handleNetworkConnection(now);
|
||||
if (network_just_connected) {
|
||||
// The uplink recovered — reset last_reconnect_attempt for disconnected slots so they
|
||||
// retry immediately rather than waiting up to 5 min for backoff timers to expire.
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (_slots[i].enabled && _slots[i].initial_connect_done && !_slots[i].connected) {
|
||||
@@ -1373,14 +1315,18 @@ void MQTTBridge::mqttTaskLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pending NTP sync (triggered from WiFi event handler)
|
||||
if (_ntp_sync_pending && WiFi.status() == WL_CONNECTED) {
|
||||
// A connected observation is enough to schedule NTP; physical event callbacks
|
||||
// stay encapsulated in the selected network adapter.
|
||||
if (!_ntp_synced && _network->isConnected() && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
if (_ntp_sync_pending && _network->isConnected()) {
|
||||
_ntp_sync_pending = false;
|
||||
syncTimeWithNTP();
|
||||
}
|
||||
|
||||
// Retry NTP every 30s if initial sync failed (slots can't start without valid time)
|
||||
if (!_ntp_synced && WiFi.status() == WL_CONNECTED) {
|
||||
if (!_ntp_synced && _network->isConnected()) {
|
||||
static unsigned long last_ntp_retry = 0;
|
||||
if (now - last_ntp_retry >= 30000) {
|
||||
last_ntp_retry = now;
|
||||
@@ -1495,7 +1441,7 @@ void MQTTBridge::mqttTaskLoop() {
|
||||
#ifdef WITH_SNMP
|
||||
// SNMP agent loop — process incoming UDP requests
|
||||
if (_snmp_agent) {
|
||||
if (!_snmp_agent->isRunning() && WiFi.isConnected() && _obs->snmp_enabled) {
|
||||
if (!_snmp_agent->isRunning() && _network->isConnected() && _obs->snmp_enabled) {
|
||||
_snmp_agent->begin(_obs->snmp_community);
|
||||
MQTT_DEBUG_PRINTLN("SNMP agent started on port 161 (community: %s)", _obs->snmp_community);
|
||||
}
|
||||
@@ -1517,7 +1463,7 @@ void MQTTBridge::mqttTaskLoop() {
|
||||
// Periodic NTP refresh (every hour) — lightweight, non-blocking.
|
||||
// Uses async SNTP instead of the heavy syncTimeWithNTP() which blocks Core 0
|
||||
// for up to 20+ seconds with DNS lookups, UDP sockets, and retry loops.
|
||||
if (WiFi.status() == WL_CONNECTED && now - _last_ntp_sync > 3600000) {
|
||||
if (_network->isConnected() && now - _last_ntp_sync > 3600000) {
|
||||
refreshNTP();
|
||||
}
|
||||
|
||||
@@ -2062,7 +2008,7 @@ void MQTTBridge::maintainSlotConnections() {
|
||||
if (!_identity) return;
|
||||
|
||||
// Check WiFi status first
|
||||
if (WiFi.status() != WL_CONNECTED) return;
|
||||
if (!_network->isConnected()) return;
|
||||
|
||||
unsigned long now_millis = millis();
|
||||
unsigned long current_time = time(nullptr);
|
||||
@@ -2792,98 +2738,26 @@ void MQTTBridge::checkConfigurationMismatch() {
|
||||
}
|
||||
}
|
||||
|
||||
bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
wl_status_t current_wifi_status = WiFi.status();
|
||||
bool transitioned_to_connected = false;
|
||||
|
||||
if (current_wifi_status == WL_CONNECTED && s_wifi_connected_at == 0) {
|
||||
s_wifi_connected_at = now;
|
||||
}
|
||||
if (!_wifi_status_initialized) {
|
||||
_last_wifi_status = current_wifi_status;
|
||||
_wifi_status_initialized = true;
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, current_wifi_status == WL_CONNECTED, wifiOutage(), false));
|
||||
}
|
||||
if (now - _last_wifi_check <= 10000) {
|
||||
// Events own the snapshot between 10 s polls. If STA is associated again
|
||||
// and GOT_IP was missed, still close the outage so a flap contained
|
||||
// between polls does not look like one continuous downtime.
|
||||
if (current_wifi_status == WL_CONNECTED) {
|
||||
AlertFaultPolicy::OutageSnapshot snap = wifiOutage();
|
||||
if (snap.down) {
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiGotIp(snap));
|
||||
bool MQTTBridge::handleNetworkConnection(unsigned long now) {
|
||||
const NetworkTransition transition =
|
||||
_network->maintain((uint32_t)now, _obs->wifi_power_save);
|
||||
const NetworkPolicy::MQTTTransitionActions actions =
|
||||
NetworkPolicy::mqttActions(transition);
|
||||
if (actions.disconnect_slots) {
|
||||
// Broker ownership stays in the bridge. The physical adapter reports the
|
||||
// edge; the bridge explicitly closes every slot instead of waiting for
|
||||
// eventual socket timeouts.
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (_slots[i].client && _slots[i].connected) {
|
||||
_slots[i].client->disconnect();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_last_wifi_check = now;
|
||||
|
||||
if (current_wifi_status == WL_CONNECTED) {
|
||||
if (_last_wifi_status != WL_CONNECTED) {
|
||||
transitioned_to_connected = true;
|
||||
setWifiOutage(AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, true, wifiOutage(), true));
|
||||
s_wifi_connected_at = now;
|
||||
_wifi_reconnect_backoff_attempt = 0;
|
||||
#ifdef ESP_PLATFORM
|
||||
wifi_ps_type_t ps_mode;
|
||||
uint8_t ps_pref = _obs->wifi_power_save;
|
||||
if (ps_pref == 1) {
|
||||
ps_mode = WIFI_PS_NONE;
|
||||
} else if (ps_pref == 2) {
|
||||
ps_mode = WIFI_PS_MAX_MODEM;
|
||||
} else {
|
||||
ps_mode = WIFI_PS_NONE; // default: no power save; eliminates DTIM wake latency on mains-powered bridges
|
||||
}
|
||||
esp_wifi_set_ps(ps_mode);
|
||||
#ifdef MQTT_WIFI_TX_POWER
|
||||
WiFi.setTxPower(MQTT_WIFI_TX_POWER);
|
||||
#else
|
||||
WiFi.setTxPower(WIFI_POWER_11dBm);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
if (s_wifi_connected_at == 0) {
|
||||
s_wifi_connected_at = now;
|
||||
}
|
||||
_last_wifi_status = WL_CONNECTED;
|
||||
} else {
|
||||
const bool last_connected = (_last_wifi_status == WL_CONNECTED);
|
||||
AlertFaultPolicy::OutageSnapshot snap = AlertFaultPolicy::applyWifiStatus(
|
||||
(uint32_t)now, false, wifiOutage(), true);
|
||||
setWifiOutage(snap);
|
||||
if (last_connected) {
|
||||
s_wifi_connected_at = 0;
|
||||
// Disconnect all slot clients when WiFi drops
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (_slots[i].client && _slots[i].connected) {
|
||||
_slots[i].client->disconnect();
|
||||
}
|
||||
}
|
||||
} else if (snap.down) {
|
||||
// Backoff ladder + wrap-safe timing live in MQTTConnectionPolicy (Phase 6),
|
||||
// exercised by host tests. Behavior is unchanged: both the link-down
|
||||
// duration and the since-last-attempt interval must clear the current rung
|
||||
// (elapsedMs is the wrap-safe form of the old ULONG_MAX branch).
|
||||
if (MQTTConnectionPolicy::wifiReconnectDue(
|
||||
(uint32_t)now, snap.started_ms,
|
||||
(uint32_t)_last_wifi_reconnect_attempt,
|
||||
_wifi_reconnect_backoff_attempt)) {
|
||||
_last_wifi_reconnect_attempt = now;
|
||||
_wifi_reconnect_backoff_attempt =
|
||||
MQTTConnectionPolicy::nextWifiBackoffAttempt(_wifi_reconnect_backoff_attempt);
|
||||
WiFi.disconnect();
|
||||
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
|
||||
}
|
||||
}
|
||||
_last_wifi_status = current_wifi_status;
|
||||
}
|
||||
return transitioned_to_connected;
|
||||
return actions.retry_disconnected_slots_now;
|
||||
}
|
||||
|
||||
bool MQTTBridge::isReady() const {
|
||||
return _initialized && isWiFiConfigValid(_obs);
|
||||
return _initialized && isNetworkConfigValid(_obs);
|
||||
}
|
||||
|
||||
bool MQTTBridge::isIATAValid() const {
|
||||
@@ -2943,10 +2817,10 @@ void MQTTBridge::loop() {
|
||||
return;
|
||||
#else
|
||||
unsigned long now = millis();
|
||||
if (handleWiFiConnection(now) && !_ntp_synced) {
|
||||
if (handleNetworkConnection(now) && !_ntp_synced) {
|
||||
syncTimeWithNTP();
|
||||
}
|
||||
if (_ntp_sync_pending && WiFi.status() == WL_CONNECTED) {
|
||||
if (_ntp_sync_pending && _network->isConnected()) {
|
||||
_ntp_sync_pending = false;
|
||||
syncTimeWithNTP();
|
||||
}
|
||||
@@ -2986,7 +2860,7 @@ void MQTTBridge::loop() {
|
||||
checkConfigurationMismatch();
|
||||
|
||||
// Periodic NTP refresh (every hour) — lightweight, non-blocking.
|
||||
if (WiFi.status() == WL_CONNECTED && millis() - _last_ntp_sync > 3600000) {
|
||||
if (_network->isConnected() && millis() - _last_ntp_sync > 3600000) {
|
||||
refreshNTP();
|
||||
}
|
||||
|
||||
@@ -3977,8 +3851,8 @@ void MQTTBridge::refreshNTP() {
|
||||
}
|
||||
|
||||
bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
if (!WiFi.isConnected()) {
|
||||
MQTT_DEBUG_PRINTLN("Cannot sync time - WiFi not connected");
|
||||
if (!_network->isConnected()) {
|
||||
MQTT_DEBUG_PRINTLN("Cannot sync time - %s not connected", _network->mediumName());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4026,7 +3900,7 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
|
||||
// Skipping is what keeps the credit honest; the name that answered is the name
|
||||
// recorded.
|
||||
IPAddress resolved_ip;
|
||||
if (!WiFi.hostByName(server, resolved_ip)) {
|
||||
if (!_network->resolveHost(server, resolved_ip)) {
|
||||
MQTT_DEBUG_PRINTLN("NTP: %s does not resolve — skipping, not attempting a send", server);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "MeshCore.h"
|
||||
#include "helpers/bridges/BridgeBase.h"
|
||||
#include "helpers/NetworkInterface.h"
|
||||
#include <PsychicMqttClient.h>
|
||||
#include <WiFi.h>
|
||||
#include <NTPClient.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <Timezone.h>
|
||||
@@ -219,11 +219,6 @@ private:
|
||||
bool _ntp_synced;
|
||||
bool _ntp_sync_pending; // Flag to trigger NTP sync from loop() instead of event handler
|
||||
bool _slots_setup_done; // Deferred: slots set up after NTP sync
|
||||
// WiFi.onEvent() handler registered once and never removed by end(); the bridge
|
||||
// object is reused across restarts, so re-registering would leak handlers and
|
||||
// duplicate every connect/disconnect log line. Inline-initialised so it survives
|
||||
// construction and is NOT reset by end().
|
||||
bool _wifi_event_registered = false;
|
||||
int _max_active_slots; // Runtime limit: 5 with PSRAM, 2 without
|
||||
|
||||
// Pending slot reconfigure: set from CLI (Core 1), processed by MQTT task (Core 0)
|
||||
@@ -406,25 +401,8 @@ private:
|
||||
unsigned long _last_config_warning; // Throttle configuration mismatch warnings
|
||||
static const unsigned long CONFIG_WARNING_INTERVAL = 300000; // Log every 5 minutes max
|
||||
|
||||
// WiFi connection state and exponential backoff
|
||||
unsigned long _last_wifi_check;
|
||||
wl_status_t _last_wifi_status;
|
||||
bool _wifi_status_initialized;
|
||||
// Packed OutageSnapshot; Core 0 (event + MQTT task) stores, Core 1 loads.
|
||||
std::atomic<uint64_t> _wifi_outage_bits;
|
||||
unsigned long _last_wifi_reconnect_attempt;
|
||||
uint8_t _wifi_reconnect_backoff_attempt; // 0..5 → 15s, 30s, 60s, 120s, 300s; reset on connect
|
||||
unsigned long _last_slot_reconnect_ms; // guards against concurrent TLS handshakes (15 s inter-slot gap)
|
||||
|
||||
AlertFaultPolicy::OutageSnapshot wifiOutage() const {
|
||||
return AlertFaultPolicy::unpackOutageSnapshot(
|
||||
_wifi_outage_bits.load(std::memory_order_acquire));
|
||||
}
|
||||
void setWifiOutage(AlertFaultPolicy::OutageSnapshot snap) {
|
||||
_wifi_outage_bits.store(AlertFaultPolicy::packOutageSnapshot(snap),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
// Optional pointers for collecting stats internally (set by mesh if available)
|
||||
mesh::Dispatcher* _dispatcher; // For air times and errors
|
||||
mesh::Radio* _radio; // For noise floor
|
||||
@@ -483,13 +461,13 @@ private:
|
||||
|
||||
void processPacketQueue();
|
||||
bool publishStatus(); // Returns true if status was successfully published
|
||||
bool handleWiFiConnection(unsigned long now);
|
||||
bool handleNetworkConnection(unsigned long now);
|
||||
|
||||
// FreeRTOS task function (runs on Core 0)
|
||||
#ifdef ESP_PLATFORM
|
||||
static void mqttTask(void* parameter);
|
||||
void mqttTaskLoop(); // Main loop for MQTT task
|
||||
void initializeWiFiInTask(); // WiFi initialization moved to task
|
||||
void initializeNetworkInTask(); // Selected-link initialization moved to task
|
||||
#endif
|
||||
bool publishPacket(mesh::Packet* packet, bool is_tx, bool& has_eligible_target,
|
||||
const uint8_t* raw_data = nullptr, int raw_len = 0,
|
||||
@@ -547,6 +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;
|
||||
|
||||
public:
|
||||
MQTTBridge(NodePrefs *prefs, MQTTPrefs *obs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity);
|
||||
@@ -655,13 +634,17 @@ public:
|
||||
static unsigned long getWifiConnectedAtMillis();
|
||||
|
||||
/**
|
||||
* Current WiFi outage snapshot for AlertReporter: down, started_ms, and the
|
||||
* Current selected-network outage snapshot for AlertReporter: down,
|
||||
* started_ms, and the
|
||||
* initiating disconnect reason. Distinct from getLastWifiDisconnectTime() /
|
||||
* getLastWifiDisconnectReason(), which follow the most recent ESP-IDF
|
||||
* DISCONNECTED event and are overwritten by STA-backoff WiFi.disconnect()
|
||||
* (reason 8 / ASSOC_LEAVE).
|
||||
*/
|
||||
AlertFaultPolicy::OutageSnapshot getWifiOutageSnapshot() const { return wifiOutage(); }
|
||||
AlertFaultPolicy::OutageSnapshot getWifiOutageSnapshot() const {
|
||||
return _network ? _network->outageSnapshot()
|
||||
: AlertFaultPolicy::OutageSnapshot{false, 0, 0};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-slot outage accessors used by AlertReporter to detect prolonged
|
||||
@@ -723,7 +706,7 @@ public:
|
||||
uint16_t filter_mask;
|
||||
};
|
||||
static bool getSlotStatusSnapshot(int slot_index, SlotStatusSnapshot* out);
|
||||
/** True when WiFi is set and at least one MQTT slot can run (preset + custom host if needed). */
|
||||
/** True when the selected network is configured and at least one MQTT slot can run. */
|
||||
static bool isConfigValid(const MQTTPrefs* obs);
|
||||
static void formatSlotDiagReply(char* buf, size_t bufsize, int slot_index);
|
||||
static uint8_t getLastWifiDisconnectReason();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <ESP32_CH390.h>
|
||||
|
||||
/**
|
||||
* Bring up the repository's CH390 lwIP interface from the board build flags.
|
||||
* Shared by the companion transport wrapper and the observer network adapter so
|
||||
* pin and static-IP behavior cannot drift between the two paths.
|
||||
*/
|
||||
static inline bool beginConfiguredCH390() {
|
||||
ch390_config_t config = CH390_DEFAULT_CONFIG();
|
||||
config.spi_miso_gpio = ETH_MISO_PIN;
|
||||
config.spi_mosi_gpio = ETH_MOSI_PIN;
|
||||
config.spi_sck_gpio = ETH_SCLK_PIN;
|
||||
config.spi_cs_gpio = ETH_CS_PIN;
|
||||
config.int_gpio = ETH_INT_PIN;
|
||||
if (!CH390.begin(config)) return false;
|
||||
|
||||
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET)
|
||||
IPAddress ip(ETHERNET_STATIC_IP);
|
||||
IPAddress gateway(ETHERNET_STATIC_GATEWAY);
|
||||
IPAddress subnet(ETHERNET_STATIC_SUBNET);
|
||||
#if defined(ETHERNET_STATIC_DNS)
|
||||
IPAddress dns(ETHERNET_STATIC_DNS);
|
||||
CH390.config(ip, gateway, subnet, dns);
|
||||
#else
|
||||
CH390.config(ip, gateway, subnet);
|
||||
#endif
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "CH390EthernetInterface.h"
|
||||
#include "CH390Config.h"
|
||||
|
||||
void onWiFiEvent(WiFiEvent_t event) {
|
||||
switch(event){
|
||||
@@ -29,26 +30,12 @@ bool CH390EthernetInterface::begin() {
|
||||
// listen to ethernet events
|
||||
WiFi.onEvent(onWiFiEvent);
|
||||
|
||||
// Init CH390
|
||||
ch390_config_t config = CH390_DEFAULT_CONFIG();
|
||||
config.spi_miso_gpio = ETH_MISO_PIN;
|
||||
config.spi_mosi_gpio = ETH_MOSI_PIN;
|
||||
config.spi_sck_gpio = ETH_SCLK_PIN;
|
||||
config.spi_cs_gpio = ETH_CS_PIN;
|
||||
config.int_gpio = ETH_INT_PIN;
|
||||
if (!CH390.begin(config)) {
|
||||
// Init CH390 using the same board configuration as the observer uplink.
|
||||
if (!beginConfiguredCH390()) {
|
||||
ETHERNET_DEBUG_PRINTLN("Failed to initialize CH390 hardware.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup Static IP if build flags are present
|
||||
#if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET)
|
||||
IPAddress ip(ETHERNET_STATIC_IP);
|
||||
IPAddress gw(ETHERNET_STATIC_GATEWAY);
|
||||
IPAddress sn(ETHERNET_STATIC_SUBNET);
|
||||
CH390.config(ip, gw, sn);
|
||||
#endif
|
||||
|
||||
// Start Server
|
||||
server.begin(ETHERNET_TCP_PORT);
|
||||
ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT);
|
||||
|
||||
@@ -29,6 +29,7 @@ does not reflect the GoogleTest count — run the built binary directly
|
||||
| `test_topic_template` | `src/helpers/MQTTTopicTemplate.h` | `{iata}/{device}/{token}/{type}` expansion, overflow/NUL-termination, and a buffer-size fuzz |
|
||||
| `test_mqtt_topic_router` | `src/helpers/MQTTTopicRouter.h` | complete preset/custom topic-routing contract; MeshRank all types except raw; required identifiers; invalid inputs/slots; exact buffer boundaries |
|
||||
| `test_mqtt_connection_policy` | `src/helpers/MQTTConnectionPolicy.h` | reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit `millis()` rollover; WiFi current-outage start sticky across STA reconnect attempts |
|
||||
| `test_network_policy` | `src/helpers/NetworkPolicy.h` | transport-neutral MQTT link-transition actions and `start ota` selected-LAN versus forced/fallback SoftAP choice |
|
||||
| `test_alert_fault_policy` | `src/helpers/AlertFaultPolicy.h` | WiFi/MQTT fault edge detector; `OutageSnapshot` (down / started_ms / initiating reason) fed to tick and `formatWifiAlert`; reason-8 reconnects change neither duration nor initiating reason; flap between status polls; down at `millis()==0`; packed 64-bit cross-task word; rate-limit floor and first-fire; 5 s poll cadence and `millis()` rollover |
|
||||
| `test_display_viewport` | `src/helpers/ui/DisplayViewport.h`, `src/helpers/ui/DisplayFrameSignature.h` | logical-to-physical portrait mapping; fractional span coverage; fitted-width conversion; preferred/fallback text scaling; stable visible-frame change detection |
|
||||
| `test_mqtt_packet_queue_policy` | `src/helpers/MQTTPacketQueuePolicy.h` | queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit `millis()` rollover |
|
||||
|
||||
@@ -350,6 +350,17 @@ TEST(AlertFaultPolicy, FormatWifiAlertUsesSnapshotReason) {
|
||||
EXPECT_STREQ("WiFi down 47m", text);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, FormatNetworkAlertUsesSelectedMediumLabel) {
|
||||
Alert::Fault f = OkFault();
|
||||
const Alert::OutageSnapshot snap = Down(1000, 0);
|
||||
const Alert::TickResult r = Alert::tick(
|
||||
f, 1000 + kWifiThresh, snap, kWifiThresh, kMinInterval);
|
||||
char text[80];
|
||||
ASSERT_TRUE(Alert::formatNetworkAlert(
|
||||
text, sizeof(text), "Ethernet", r, snap));
|
||||
EXPECT_STREQ("Ethernet down 30m", text);
|
||||
}
|
||||
|
||||
TEST(AlertFaultPolicy, FormatMqttSlotMessages) {
|
||||
char text[100];
|
||||
Alert::formatMqttDown(text, sizeof(text), 1, "analyzer-us", 30U * 60000U);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helpers/NetworkPolicy.h"
|
||||
|
||||
TEST(NetworkPolicy, MqttDownDisconnectsSlotsWithoutRequestingImmediateRetry) {
|
||||
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Down);
|
||||
EXPECT_TRUE(actions.disconnect_slots);
|
||||
EXPECT_FALSE(actions.retry_disconnected_slots_now);
|
||||
}
|
||||
|
||||
TEST(NetworkPolicy, MqttUpRequestsImmediateRetryWithoutDisconnectingSlots) {
|
||||
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Up);
|
||||
EXPECT_FALSE(actions.disconnect_slots);
|
||||
EXPECT_TRUE(actions.retry_disconnected_slots_now);
|
||||
}
|
||||
|
||||
TEST(NetworkPolicy, NoTransitionHasNoMqttSideEffects) {
|
||||
const auto actions = NetworkPolicy::mqttActions(NetworkTransition::None);
|
||||
EXPECT_FALSE(actions.disconnect_slots);
|
||||
EXPECT_FALSE(actions.retry_disconnected_slots_now);
|
||||
}
|
||||
|
||||
TEST(NetworkPolicy, StartOtaUsesReachableSelectedNetworkByDefault) {
|
||||
EXPECT_TRUE(NetworkPolicy::startOtaUsesSelectedNetwork(false, true));
|
||||
EXPECT_FALSE(NetworkPolicy::startOtaUsesSelectedNetwork(false, false));
|
||||
}
|
||||
|
||||
TEST(NetworkPolicy, StartOtaForceApOverridesAReachableSelectedNetwork) {
|
||||
EXPECT_FALSE(NetworkPolicy::startOtaUsesSelectedNetwork(true, true));
|
||||
EXPECT_FALSE(NetworkPolicy::startOtaUsesSelectedNetwork(true, false));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -37,22 +37,31 @@ build_src_filter = ${esp32_base.build_src_filter}
|
||||
lib_deps = ${esp32_base.lib_deps}
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
|
||||
[ThinkNode_M7_ethernet]
|
||||
[ThinkNode_M7_ch390]
|
||||
build_flags =
|
||||
-D ETHERNET_ENABLED
|
||||
-D ETHERNET_USE_CH390
|
||||
-D ETHERNET_CLASS=CH390EthernetInterface
|
||||
-D ETH_MISO_PIN=14
|
||||
-D ETH_MOSI_PIN=48
|
||||
-D ETH_SCLK_PIN=47
|
||||
-D ETH_CS_PIN=21
|
||||
-D ETH_INT_PIN=45
|
||||
-D ETHERNET_DEBUG_LOGGING=1
|
||||
lib_deps =
|
||||
https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d
|
||||
|
||||
; Existing companion/CLI transport overlay. MQTT Ethernet observers consume the
|
||||
; CH390 fragment above without ETHERNET_ENABLED, whose meaning in the simple
|
||||
; repeater/room entry points is the legacy nRF52 Ethernet CLI.
|
||||
[ThinkNode_M7_ethernet]
|
||||
build_flags =
|
||||
${ThinkNode_M7_ch390.build_flags}
|
||||
-D ETHERNET_ENABLED
|
||||
-D ETHERNET_CLASS=CH390EthernetInterface
|
||||
build_src_filter =
|
||||
+<helpers/ethernet/*.cpp>
|
||||
+<helpers/ethernet/ch390/>
|
||||
lib_deps =
|
||||
https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d
|
||||
${ThinkNode_M7_ch390.lib_deps}
|
||||
|
||||
[env:ThinkNode_M7_repeater]
|
||||
extends = ThinkNode_M7
|
||||
@@ -180,9 +189,10 @@ extends = ThinkNode_M7
|
||||
build_src_filter = ${ThinkNode_M7.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
; MQTT observer envs are WiFi-only: the bridge's link management is bound to the
|
||||
; WiFi station API, so the onboard CH390 cannot carry MQTT yet. The board has
|
||||
; PSRAM, so MAX_NEIGHBOURS enables WITH_MQTT_NEIGHBORS (see MQTTBridge.h).
|
||||
; Wi-Fi remains the default observer transport. Ethernet twins below select the
|
||||
; onboard CH390 through NETWORK_USE_ETHERNET without enabling the legacy CLI
|
||||
; transport. The board has PSRAM, so MAX_NEIGHBOURS enables
|
||||
; WITH_MQTT_NEIGHBORS (see MQTTBridge.h).
|
||||
[env:ThinkNode_M7_repeater_observer_mqtt]
|
||||
extends = ThinkNode_M7
|
||||
extra_scripts =
|
||||
@@ -227,6 +237,17 @@ lib_deps =
|
||||
paulstoffregen/Time@1.6.1
|
||||
0neblock/SNMP_Agent
|
||||
|
||||
[env:ThinkNode_M7_repeater_observer_mqtt_ethernet]
|
||||
extends = env:ThinkNode_M7_repeater_observer_mqtt
|
||||
build_flags =
|
||||
${env:ThinkNode_M7_repeater_observer_mqtt.build_flags}
|
||||
${ThinkNode_M7_ch390.build_flags}
|
||||
-D NETWORK_USE_ETHERNET=1
|
||||
build_src_filter = ${env:ThinkNode_M7_repeater_observer_mqtt.build_src_filter}
|
||||
lib_deps =
|
||||
${env:ThinkNode_M7_repeater_observer_mqtt.lib_deps}
|
||||
${ThinkNode_M7_ch390.lib_deps}
|
||||
|
||||
[env:ThinkNode_M7_room_server_observer_mqtt]
|
||||
extends = ThinkNode_M7
|
||||
extra_scripts =
|
||||
@@ -271,3 +292,14 @@ lib_deps =
|
||||
JChristensen/Timezone
|
||||
paulstoffregen/Time@1.6.1
|
||||
0neblock/SNMP_Agent
|
||||
|
||||
[env:ThinkNode_M7_room_server_observer_mqtt_ethernet]
|
||||
extends = env:ThinkNode_M7_room_server_observer_mqtt
|
||||
build_flags =
|
||||
${env:ThinkNode_M7_room_server_observer_mqtt.build_flags}
|
||||
${ThinkNode_M7_ch390.build_flags}
|
||||
-D NETWORK_USE_ETHERNET=1
|
||||
build_src_filter = ${env:ThinkNode_M7_room_server_observer_mqtt.build_src_filter}
|
||||
lib_deps =
|
||||
${env:ThinkNode_M7_room_server_observer_mqtt.lib_deps}
|
||||
${ThinkNode_M7_ch390.lib_deps}
|
||||
Reference in new issue
Block a user