mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 06:22:48 +00:00
Fix MQTT startup after ESP32 WebConfig provisioning
This commit is contained in:
1 parent
745aeb1758
commit
1e421ff4fc
18 files changed
+1018
-208
No files matched your search
@@ -480,6 +480,8 @@ jobs:
|
||||
python3 -B test/test_webconfig_gzip.py
|
||||
python3 -B test/test_webconfig_secret_masking.py
|
||||
python3 -B test/test_webconfig_station_ownership.py -v
|
||||
python3 -B test/test_mqtt_canonical_wifi.py -v
|
||||
python3 -B test/test_heltec_v4_wifi_setup_page.py -v
|
||||
python3 -B test/test_sensor_webconfig_capabilities.py -v
|
||||
python3 -B test/test_webconfig_ui_runtime.py
|
||||
|
||||
|
||||
@@ -3851,6 +3851,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
#endif
|
||||
@@ -3939,8 +3942,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
// Preserve the MQTT observer's first-boot setup experience even though the
|
||||
// persistent WebUI master switch defaults off on infrastructure roles.
|
||||
start_webui = start_webui || (_prefs.bridge_enabled
|
||||
&& _cli.getObserverPrefs()->wifi_ssid[0] == 0);
|
||||
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
|
||||
&& !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs()));
|
||||
if (start_webui && !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())) {
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
if (espnow_bridge.isRunning()) espnow_bridge.end();
|
||||
#endif
|
||||
@@ -10931,7 +10934,7 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
|
||||
void MyMesh::suspendUnconfiguredSetupBridges() {
|
||||
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_cli.getObserverPrefs()->wifi_ssid[0]) return;
|
||||
if (WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())) return;
|
||||
#else
|
||||
char ssid[33] = {};
|
||||
WebConfigServer::loadStandaloneWiFi(ssid, sizeof(ssid), nullptr, 0);
|
||||
@@ -10974,7 +10977,7 @@ bool MyMesh::startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup
|
||||
// out. Require an explicit stop before a portal that would take its radio.
|
||||
if ((isMqttBridgeRunning() || isMqttBridgeStopping()) && (force_ap
|
||||
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|
||||
|| _cli.getObserverPrefs()->wifi_ssid[0] == 0
|
||||
|| !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())
|
||||
#endif
|
||||
)) {
|
||||
strcpy(reply, isMqttBridgeStopping() ? "Err: MQTT bridge is stopping - retry shortly"
|
||||
@@ -11088,15 +11091,15 @@ bool MyMesh::getWebUIStatus(char* reply) const {
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiSSID(char* reply) const {
|
||||
return WebConfigServer::formatWiFiSSID(reply, 160);
|
||||
return WebConfigServer::formatWiFiSSID(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiStatus(char* reply) const {
|
||||
return WebConfigServer::formatWiFiStatus(reply, 160);
|
||||
return WebConfigServer::formatWiFiStatus(reply, 160, nullptr, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiPowerSave(char* reply) const {
|
||||
return WebConfigServer::formatWiFiPowerSave(reply, 160);
|
||||
return WebConfigServer::formatWiFiPowerSave(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiCLI(char* reply) const {
|
||||
@@ -11104,7 +11107,7 @@ bool MyMesh::getWiFiCLI(char* reply) const {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiSSID(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiSSID(value, reply, 160)) {
|
||||
if (WebConfigServer::setStandaloneWiFiSSID(value, reply, 160, canonicalWiFiLegacyPrefs())) {
|
||||
const bool was_running = _webconfig && _webconfig->isRunning();
|
||||
if (was_running) _webconfig->requestStop();
|
||||
if (_webconfig) _webconfig->reloadStandaloneWiFi();
|
||||
@@ -11116,7 +11119,7 @@ bool MyMesh::setWiFiSSID(const char* value, char* reply) {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiPassword(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiPassword(value, reply, 160)) {
|
||||
if (WebConfigServer::setStandaloneWiFiPassword(value, reply, 160, canonicalWiFiLegacyPrefs())) {
|
||||
const bool was_running = _webconfig && _webconfig->isRunning();
|
||||
if (was_running) _webconfig->requestStop();
|
||||
if (_webconfig) _webconfig->reloadStandaloneWiFi();
|
||||
@@ -11128,7 +11131,7 @@ bool MyMesh::setWiFiPassword(const char* value, char* reply) {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiPowerSave(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiPowerSave(value, reply, 160)
|
||||
if (WebConfigServer::setStandaloneWiFiPowerSave(value, reply, 160, canonicalWiFiLegacyPrefs())
|
||||
&& _webconfig) {
|
||||
_webconfig->reloadStandaloneWiFi();
|
||||
}
|
||||
|
||||
@@ -1377,6 +1377,9 @@ public:
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
if (!mqtt_bridge) return false;
|
||||
@@ -1486,6 +1489,9 @@ public:
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
if (!mqtt_bridge) return false;
|
||||
@@ -1743,9 +1749,17 @@ public:
|
||||
}
|
||||
bool setWebUIEnabled(bool enabled, char* reply) override;
|
||||
bool getWebUIStatus(char* reply) const override;
|
||||
bool usesCanonicalWiFi() const override { return true; }
|
||||
const void* canonicalWiFiLegacyPrefs() const {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
return _cli.getObserverPrefs();
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
bool getWiFiSSID(char* reply) const override;
|
||||
bool getWiFiPassword(char* reply) const override {
|
||||
return WebConfigServer::formatWiFiPassword(reply, 160);
|
||||
return WebConfigServer::formatWiFiPassword(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
bool getWiFiStatus(char* reply) const override;
|
||||
bool getWiFiPowerSave(char* reply) const override;
|
||||
|
||||
@@ -1453,6 +1453,9 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
if (bridge) {
|
||||
@@ -1501,8 +1504,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
bool start_webui = WebConfigServer::loadEnabled(false);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
start_webui = start_webui || (_prefs.bridge_enabled
|
||||
&& _cli.getObserverPrefs()->wifi_ssid[0] == 0);
|
||||
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
|
||||
&& !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs()));
|
||||
if (start_webui && !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())) {
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
if (espnow_bridge.isRunning()) espnow_bridge.end();
|
||||
#endif
|
||||
@@ -1956,7 +1959,7 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
|
||||
void MyMesh::suspendUnconfiguredSetupBridges() {
|
||||
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_cli.getObserverPrefs()->wifi_ssid[0]) return;
|
||||
if (WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())) return;
|
||||
#else
|
||||
char ssid[33] = {};
|
||||
WebConfigServer::loadStandaloneWiFi(ssid, sizeof(ssid), nullptr, 0);
|
||||
@@ -1993,7 +1996,7 @@ bool MyMesh::startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if ((isMqttBridgeRunning() || isMqttBridgeStopping()) && (force_ap
|
||||
#if defined(MESHCORE_EXPANDED_PARTITION_PROFILE)
|
||||
|| _cli.getObserverPrefs()->wifi_ssid[0] == 0
|
||||
|| !WebConfigServer::hasConfiguredWiFi(_cli.getObserverPrefs())
|
||||
#endif
|
||||
)) {
|
||||
strcpy(reply, isMqttBridgeStopping() ? "Err: MQTT bridge is stopping - retry shortly"
|
||||
@@ -2094,15 +2097,15 @@ bool MyMesh::getWebUIStatus(char* reply) const {
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiSSID(char* reply) const {
|
||||
return WebConfigServer::formatWiFiSSID(reply, 160);
|
||||
return WebConfigServer::formatWiFiSSID(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiStatus(char* reply) const {
|
||||
return WebConfigServer::formatWiFiStatus(reply, 160);
|
||||
return WebConfigServer::formatWiFiStatus(reply, 160, nullptr, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiPowerSave(char* reply) const {
|
||||
return WebConfigServer::formatWiFiPowerSave(reply, 160);
|
||||
return WebConfigServer::formatWiFiPowerSave(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
|
||||
bool MyMesh::getWiFiCLI(char* reply) const {
|
||||
@@ -2110,7 +2113,7 @@ bool MyMesh::getWiFiCLI(char* reply) const {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiSSID(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiSSID(value, reply, 160)) {
|
||||
if (WebConfigServer::setStandaloneWiFiSSID(value, reply, 160, canonicalWiFiLegacyPrefs())) {
|
||||
const bool was_running = _webconfig && _webconfig->isRunning();
|
||||
if (was_running) _webconfig->requestStop();
|
||||
if (_webconfig) _webconfig->reloadStandaloneWiFi();
|
||||
@@ -2122,7 +2125,7 @@ bool MyMesh::setWiFiSSID(const char* value, char* reply) {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiPassword(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiPassword(value, reply, 160)) {
|
||||
if (WebConfigServer::setStandaloneWiFiPassword(value, reply, 160, canonicalWiFiLegacyPrefs())) {
|
||||
const bool was_running = _webconfig && _webconfig->isRunning();
|
||||
if (was_running) _webconfig->requestStop();
|
||||
if (_webconfig) _webconfig->reloadStandaloneWiFi();
|
||||
@@ -2134,7 +2137,7 @@ bool MyMesh::setWiFiPassword(const char* value, char* reply) {
|
||||
}
|
||||
|
||||
bool MyMesh::setWiFiPowerSave(const char* value, char* reply) {
|
||||
if (WebConfigServer::setStandaloneWiFiPowerSave(value, reply, 160)
|
||||
if (WebConfigServer::setStandaloneWiFiPowerSave(value, reply, 160, canonicalWiFiLegacyPrefs())
|
||||
&& _webconfig) {
|
||||
_webconfig->reloadStandaloneWiFi();
|
||||
}
|
||||
|
||||
@@ -635,6 +635,9 @@ public:
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
if (!bridge) return false;
|
||||
@@ -741,6 +744,9 @@ public:
|
||||
node_info.cr = &_prefs.cr;
|
||||
node_info.repeat_flag = &_prefs.disable_fwd;
|
||||
node_info.repeat_when_nonzero = false;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node_info.canonical_wifi = true;
|
||||
#endif
|
||||
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
||||
getRTCClock(), &self_id);
|
||||
#endif
|
||||
@@ -899,9 +905,17 @@ public:
|
||||
}
|
||||
bool setWebUIEnabled(bool enabled, char* reply) override;
|
||||
bool getWebUIStatus(char* reply) const override;
|
||||
bool usesCanonicalWiFi() const override { return true; }
|
||||
const void* canonicalWiFiLegacyPrefs() const {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
return _cli.getObserverPrefs();
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
bool getWiFiSSID(char* reply) const override;
|
||||
bool getWiFiPassword(char* reply) const override {
|
||||
return WebConfigServer::formatWiFiPassword(reply, 160);
|
||||
return WebConfigServer::formatWiFiPassword(reply, 160, canonicalWiFiLegacyPrefs());
|
||||
}
|
||||
bool getWiFiStatus(char* reply) const override;
|
||||
bool getWiFiPowerSave(char* reply) const override;
|
||||
|
||||
@@ -655,6 +655,7 @@ public:
|
||||
(void)reply;
|
||||
return false;
|
||||
};
|
||||
virtual bool usesCanonicalWiFi() const { return false; }
|
||||
virtual bool getWiFiSSID(char* reply) const {
|
||||
(void)reply;
|
||||
return false;
|
||||
|
||||
@@ -183,6 +183,14 @@ static void formatMQTTPresetListReply(char* reply, size_t reply_size, int start)
|
||||
|
||||
bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_callbacks->usesCanonicalWiFi()) {
|
||||
const char* value = nullptr;
|
||||
const auto key = mesh::cli::classifyStandaloneWiFiSet(config, &value);
|
||||
if (key == mesh::cli::StandaloneWiFiKey::SSID) { _callbacks->setWiFiSSID(value, reply); return true; }
|
||||
if (key == mesh::cli::StandaloneWiFiKey::Password) { _callbacks->setWiFiPassword(value, reply); return true; }
|
||||
if (key == mesh::cli::StandaloneWiFiKey::PowerSave) { _callbacks->setWiFiPowerSave(value, reply); return true; }
|
||||
}
|
||||
|
||||
bool handled = true;
|
||||
const auto restart_observer_bridge = [this]() {
|
||||
return mesh::cli::restartBridgeIfEnabled(
|
||||
@@ -872,6 +880,21 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
|
||||
|
||||
bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_callbacks->usesCanonicalWiFi()) {
|
||||
if (strcmp(config, "wifi.pwd") == 0) {
|
||||
_callbacks->getWiFiPassword(reply);
|
||||
if (sender_timestamp) {
|
||||
const bool has_password = strlen(reply) > 2;
|
||||
strcpy(reply, has_password ? "> ******** (local connection only)" : "> (not set)");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const auto key = mesh::cli::classifyStandaloneWiFiGet(config);
|
||||
if (key == mesh::cli::StandaloneWiFiKey::SSID) { _callbacks->getWiFiSSID(reply); return true; }
|
||||
if (key == mesh::cli::StandaloneWiFiKey::Status) { _callbacks->getWiFiStatus(reply); return true; }
|
||||
if (key == mesh::cli::StandaloneWiFiKey::PowerSave) { _callbacks->getWiFiPowerSave(reply); return true; }
|
||||
}
|
||||
|
||||
bool handled = true;
|
||||
if (strcmp(config, "mqtt.enabled") == 0) {
|
||||
snprintf(reply, 160, "> %s", _prefs->bridge_enabled ? "on" : "off");
|
||||
@@ -1127,7 +1150,7 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
||||
strcpy(reply, "> (not set)");
|
||||
}
|
||||
} else if (memcmp(config, "mqtt.config.valid", 17) == 0) {
|
||||
bool valid = MQTTBridge::isConfigValid(&_mqtt_prefs);
|
||||
bool valid = MQTTBridge::isConfigValid(&_mqtt_prefs, _callbacks->usesCanonicalWiFi());
|
||||
sprintf(reply, "> %s", valid ? "valid" : "invalid");
|
||||
#endif
|
||||
} else if (memcmp(config, "alert.hashtag", 13) == 0) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#define MQTT_PRESETS_IMPLEMENTATION
|
||||
#include "MQTTBridge.h"
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <helpers/esp32/WiFiCredentials.h>
|
||||
#endif
|
||||
#include "../MQTTConnectionPolicy.h"
|
||||
#include "../MQTTMessageBuilder.h"
|
||||
#include "../MQTTPacketQueuePolicy.h"
|
||||
@@ -149,8 +152,47 @@ const char* MQTTBridge::repeatStatus() const {
|
||||
return enabled ? "on" : "off";
|
||||
}
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
void MQTTBridge::beginWiFiStation() {
|
||||
mesh::wifi::beginStation(_wifi_ssid, _wifi_password);
|
||||
}
|
||||
|
||||
bool MQTTBridge::prepareWiFiCredentials() {
|
||||
mesh::wifi::Credentials credentials;
|
||||
if (!_obs) return _wifi_configured = false;
|
||||
const auto state = _manage_wifi && _node_info.canonical_wifi
|
||||
? mesh::wifi::resolveCredentials(credentials, _obs->wifi_ssid,
|
||||
_obs->wifi_password, _obs->wifi_power_save)
|
||||
: mesh::wifi::CredentialState::Absent;
|
||||
if (!_manage_wifi || !_node_info.canonical_wifi) {
|
||||
// The Companion owns its live station; do not read infrastructure NVS.
|
||||
if (strnlen(_obs->wifi_ssid, sizeof(_obs->wifi_ssid)) >= sizeof(credentials.ssid)
|
||||
|| strnlen(_obs->wifi_password, sizeof(_obs->wifi_password)) >= sizeof(_obs->wifi_password))
|
||||
return _wifi_configured = false;
|
||||
strcpy(credentials.ssid, _obs->wifi_ssid);
|
||||
strcpy(credentials.password, _obs->wifi_password);
|
||||
credentials.power_save = _obs->wifi_power_save;
|
||||
} else if (state != mesh::wifi::CredentialState::Ready) {
|
||||
return _wifi_configured = false;
|
||||
}
|
||||
memcpy(_wifi_ssid, credentials.ssid, sizeof(_wifi_ssid));
|
||||
memcpy(_wifi_password, credentials.password, sizeof(_wifi_password));
|
||||
_wifi_power_save = credentials.power_save;
|
||||
return _wifi_configured = _wifi_ssid[0] != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper function to check if WiFi credentials are valid
|
||||
static bool isWiFiConfigValid(const MQTTPrefs* obs) {
|
||||
static bool isWiFiConfigValid(const MQTTPrefs* obs, bool canonical_wifi = false) {
|
||||
#ifdef ESP_PLATFORM
|
||||
if (canonical_wifi && obs) {
|
||||
mesh::wifi::Credentials credentials;
|
||||
return mesh::wifi::resolveCredentials(credentials, obs->wifi_ssid, obs->wifi_password,
|
||||
obs->wifi_power_save) == mesh::wifi::CredentialState::Ready;
|
||||
}
|
||||
#else
|
||||
(void)canonical_wifi;
|
||||
#endif
|
||||
// Check if WiFi SSID is configured (not empty)
|
||||
if (!obs || strlen(obs->wifi_ssid) == 0) {
|
||||
return false;
|
||||
@@ -170,8 +212,8 @@ static bool customEndpointComplete(const char* host, uint16_t port) {
|
||||
return host[0] != '\0' && (port != 0 || strstr(host, "://") != nullptr);
|
||||
}
|
||||
|
||||
bool MQTTBridge::isConfigValid(const MQTTPrefs* obs) {
|
||||
if (!obs || !isWiFiConfigValid(obs)) return false;
|
||||
bool MQTTBridge::isConfigValid(const MQTTPrefs* obs, bool canonical_wifi) {
|
||||
if (!obs || !isWiFiConfigValid(obs, canonical_wifi)) 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;
|
||||
@@ -969,7 +1011,12 @@ void MQTTBridge::begin() {
|
||||
MQTT_DEBUG_PRINTLN("Max active slots: %d", _max_active_slots);
|
||||
|
||||
// Check if WiFi credentials are configured first
|
||||
if (!isWiFiConfigValid(_obs)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
const bool wifi_configured = prepareWiFiCredentials();
|
||||
#else
|
||||
const bool wifi_configured = isWiFiConfigValid(_obs);
|
||||
#endif
|
||||
if (!wifi_configured) {
|
||||
MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - WiFi credentials not configured");
|
||||
return;
|
||||
}
|
||||
@@ -1421,7 +1468,7 @@ void MQTTBridge::initializeWiFiInTask() {
|
||||
// because _ntp_synced persists across end() (only _slots_setup_done is reset).
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
if (_manage_wifi) {
|
||||
mesh::wifi::beginStation(_obs->wifi_ssid, _obs->wifi_password);
|
||||
beginWiFiStation();
|
||||
}
|
||||
} else if (!_ntp_synced.load(std::memory_order_acquire)
|
||||
&& !_ntp_sync_pending) {
|
||||
@@ -3064,7 +3111,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
#ifdef ESP_PLATFORM
|
||||
wifi_ps_type_t ps_mode;
|
||||
uint8_t ps_pref = mesh::wifi::effectivePowerSave(
|
||||
_obs->wifi_power_save,
|
||||
_manage_wifi && _node_info.canonical_wifi ? _wifi_power_save : _obs->wifi_power_save,
|
||||
#if defined(COMPANION_EXCLUSIVE_WIFI_BLE)
|
||||
false,
|
||||
#elif defined(BLE_PIN_CODE) && defined(WIFI_SSID)
|
||||
@@ -3122,7 +3169,7 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
MQTTConnectionPolicy::nextWifiBackoffAttempt(_wifi_reconnect_backoff_attempt);
|
||||
WiFi.disconnect();
|
||||
#ifdef ESP_PLATFORM
|
||||
mesh::wifi::beginStation(_obs->wifi_ssid, _obs->wifi_password);
|
||||
beginWiFiStation();
|
||||
#else
|
||||
WiFi.begin(_obs->wifi_ssid, _obs->wifi_password);
|
||||
#endif
|
||||
@@ -3135,7 +3182,11 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
|
||||
|
||||
bool MQTTBridge::isReady() const {
|
||||
return _initialized && !_stop_requested.load(std::memory_order_acquire)
|
||||
#ifdef ESP_PLATFORM
|
||||
&& _wifi_configured;
|
||||
#else
|
||||
&& isWiFiConfigValid(_obs);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MQTTBridge::isIATAValid() const {
|
||||
|
||||
@@ -56,6 +56,8 @@ struct MQTTNodeInfo {
|
||||
const uint8_t* cr = nullptr;
|
||||
const uint8_t* repeat_flag = nullptr;
|
||||
bool repeat_when_nonzero = true;
|
||||
// ESP32 infrastructure only; Companion and Pico retain their existing owner.
|
||||
bool canonical_wifi = false;
|
||||
};
|
||||
|
||||
// Periodic neighbors publication keys off the mesh neighbor cache (sized by
|
||||
@@ -578,6 +580,15 @@ private:
|
||||
MQTTPrefs* _obs = nullptr;
|
||||
MQTTNodeInfo _node_info;
|
||||
bool _manage_wifi;
|
||||
#ifdef ESP_PLATFORM
|
||||
// Frozen before Core 0 starts; changed only after its stop barrier.
|
||||
char _wifi_ssid[32] = {};
|
||||
char _wifi_password[65] = {};
|
||||
uint8_t _wifi_power_save = 0;
|
||||
bool _wifi_configured = false;
|
||||
bool prepareWiFiCredentials();
|
||||
void beginWiFiStation();
|
||||
#endif
|
||||
|
||||
const char* repeatStatus() const;
|
||||
|
||||
@@ -779,7 +790,7 @@ public:
|
||||
};
|
||||
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). */
|
||||
static bool isConfigValid(const MQTTPrefs* obs);
|
||||
static bool isConfigValid(const MQTTPrefs* obs, bool canonical_wifi = false);
|
||||
static void formatSlotDiagReply(char* buf, size_t bufsize, int slot_index);
|
||||
static uint8_t getLastWifiDisconnectReason();
|
||||
static unsigned long getLastWifiDisconnectTime();
|
||||
|
||||
@@ -354,20 +354,9 @@ WebConfigServer::WebConfigServer(Callbacks* callbacks, void* mqtt_prefs, bool ow
|
||||
// Companion and standalone FULL builds keep their canonical connection
|
||||
// credentials in mesh-wifi NVS. Do not round-trip a 64-hex raw PSK through
|
||||
// the fixed-layout MQTTPrefs wifi_password[64] field.
|
||||
const bool loaded_standalone = loadStandaloneWiFi(
|
||||
_wifi_ssid, sizeof(_wifi_ssid),
|
||||
_wifi_password, sizeof(_wifi_password), &_wifi_power_save);
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Preserve the upgrade path from older WiFi-MQTT Companion installs that
|
||||
// have not written the canonical namespace yet. Once mesh-wifi exists it
|
||||
// always wins, including when it contains a 64-hex PSK.
|
||||
if (!loaded_standalone && obs) {
|
||||
strncpy(_wifi_ssid, obs->wifi_ssid, sizeof(_wifi_ssid) - 1);
|
||||
strncpy(_wifi_password, obs->wifi_password, sizeof(_wifi_password) - 1);
|
||||
_wifi_power_save = obs->wifi_power_save <= mesh::wifi::kPowerSaveMax
|
||||
? obs->wifi_power_save : mesh::wifi::kDefaultPowerSave;
|
||||
}
|
||||
#endif
|
||||
loadStandaloneWiFi(_wifi_ssid, sizeof(_wifi_ssid),
|
||||
_wifi_password, sizeof(_wifi_password), &_wifi_power_save,
|
||||
_mqtt_prefs);
|
||||
}
|
||||
_wifi_power_save = effectiveWiFiPowerSave(_wifi_power_save);
|
||||
}
|
||||
@@ -414,144 +403,128 @@ bool WebConfigServer::saveCliEnabled(bool enabled) {
|
||||
return written == sizeof(uint8_t);
|
||||
}
|
||||
|
||||
mesh::wifi::CredentialState WebConfigServer::resolveWiFi(
|
||||
mesh::wifi::Credentials& out, const void* legacy_prefs) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
const MQTTPrefs* obs = static_cast<const MQTTPrefs*>(legacy_prefs);
|
||||
return mesh::wifi::resolveCredentials(out, obs ? obs->wifi_ssid : nullptr,
|
||||
obs ? obs->wifi_password : nullptr,
|
||||
obs ? obs->wifi_power_save : mesh::wifi::kDefaultPowerSave);
|
||||
#else
|
||||
(void)legacy_prefs;
|
||||
return mesh::wifi::resolveCredentials(out);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WebConfigServer::hasConfiguredWiFi(const void* legacy_prefs) {
|
||||
mesh::wifi::Credentials credentials;
|
||||
return resolveWiFi(credentials, legacy_prefs) == mesh::wifi::CredentialState::Ready;
|
||||
}
|
||||
|
||||
bool WebConfigServer::loadStandaloneWiFi(char* ssid, size_t ssid_len,
|
||||
char* password, size_t password_len,
|
||||
uint8_t* power_save) {
|
||||
if (!ssid || ssid_len == 0 || !password || password_len == 0) return false;
|
||||
uint8_t* power_save, const void* legacy_prefs) {
|
||||
if (!ssid || !ssid_len || (password && !password_len)) return false;
|
||||
ssid[0] = 0;
|
||||
password[0] = 0;
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) return false;
|
||||
// Preferences::getString() logs an error for a missing key even when the
|
||||
// caller supplied a default. Missing credentials are expected on first boot.
|
||||
String stored_ssid = nvs.isKey("ssid")
|
||||
? nvs.getString("ssid", "") : String();
|
||||
String stored_password = nvs.isKey("password")
|
||||
? nvs.getString("password", "") : String();
|
||||
uint8_t stored_ps = nvs.isKey("powersave")
|
||||
? nvs.getUChar("powersave", mesh::wifi::kDefaultPowerSave)
|
||||
: mesh::wifi::kDefaultPowerSave;
|
||||
nvs.end();
|
||||
if (stored_ssid.length() >= ssid_len
|
||||
|| stored_password.length() >= password_len
|
||||
|| !mesh::cli::standaloneWiFiPasswordValid(stored_password.c_str())) {
|
||||
return false;
|
||||
}
|
||||
strncpy(ssid, stored_ssid.c_str(), ssid_len - 1);
|
||||
ssid[ssid_len - 1] = 0;
|
||||
strncpy(password, stored_password.c_str(), password_len - 1);
|
||||
password[password_len - 1] = 0;
|
||||
if (power_save) *power_save = effectiveWiFiPowerSave(stored_ps);
|
||||
return stored_ssid.length() != 0;
|
||||
if (password) password[0] = 0;
|
||||
mesh::wifi::Credentials credentials;
|
||||
const auto state = resolveWiFi(credentials, legacy_prefs);
|
||||
if (state == mesh::wifi::CredentialState::Invalid
|
||||
|| state == mesh::wifi::CredentialState::Unavailable
|
||||
|| strlen(credentials.ssid) >= ssid_len
|
||||
|| (password && strlen(credentials.password) >= password_len)) return false;
|
||||
memcpy(ssid, credentials.ssid, strlen(credentials.ssid) + 1);
|
||||
if (password) memcpy(password, credentials.password, strlen(credentials.password) + 1);
|
||||
if (power_save) *power_save = effectiveWiFiPowerSave(credentials.power_save);
|
||||
return state == mesh::wifi::CredentialState::Ready;
|
||||
}
|
||||
|
||||
bool WebConfigServer::saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
uint8_t power_save) {
|
||||
power_save = effectiveWiFiPowerSave(power_save);
|
||||
const char* pwd = password ? password : "";
|
||||
if (!ssid || !ssid[0] || strlen(ssid) >= 32
|
||||
|| !mesh::cli::standaloneWiFiPasswordValid(pwd)
|
||||
|| power_save > mesh::wifi::kPowerSaveMax) {
|
||||
if (!mesh::cli::standaloneWiFiSSIDValid(ssid)
|
||||
|| !mesh::cli::standaloneWiFiPasswordValid(password ? password : "")) return false;
|
||||
mesh::wifi::Credentials credentials;
|
||||
strcpy(credentials.ssid, ssid);
|
||||
strcpy(credentials.password, password ? password : "");
|
||||
credentials.power_save = effectiveWiFiPowerSave(power_save);
|
||||
return mesh::wifi::writeCredentials(credentials);
|
||||
}
|
||||
|
||||
static bool loadWiFiEdit(mesh::wifi::Credentials& credentials,
|
||||
const void* legacy_prefs, char* reply, size_t reply_len) {
|
||||
const auto state = WebConfigServer::resolveWiFi(credentials, legacy_prefs);
|
||||
if (state == mesh::wifi::CredentialState::Invalid
|
||||
|| state == mesh::wifi::CredentialState::Unavailable) {
|
||||
snprintf(reply, reply_len, "Error: WiFi settings unreadable; use WebConfig to save a complete pair");
|
||||
return false;
|
||||
}
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) return false;
|
||||
bool ok = nvs.putString("ssid", ssid) == strlen(ssid);
|
||||
nvs.putString("password", pwd); // empty String legitimately writes zero bytes
|
||||
ok = ok && nvs.getString("password", "\x01") == pwd;
|
||||
ok = ok && nvs.putUChar("powersave", power_save) == sizeof(uint8_t);
|
||||
nvs.end();
|
||||
return ok;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebConfigServer::setStandaloneWiFiSSID(const char* value, char* reply,
|
||||
size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
if (!mesh::cli::standaloneWiFiSSIDValid(value)) {
|
||||
size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || !reply_len) return false;
|
||||
// MQTT infrastructure historically allows clearing its SSID from the CLI.
|
||||
// Persist the complete empty canonical pair; never fall back to the old store.
|
||||
const bool clearing = legacy_prefs && value && !value[0];
|
||||
if (!clearing && !mesh::cli::standaloneWiFiSSIDValid(value)) {
|
||||
snprintf(reply, reply_len, "Error: WiFi SSID must be 1-31 characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) {
|
||||
snprintf(reply, reply_len, "Error: failed to open WiFi settings");
|
||||
return false;
|
||||
}
|
||||
const bool ok = nvs.putString("ssid", value) == strlen(value);
|
||||
nvs.end();
|
||||
snprintf(reply, reply_len, ok ? "OK - WiFi SSID saved"
|
||||
mesh::wifi::Credentials credentials;
|
||||
if (!loadWiFiEdit(credentials, legacy_prefs, reply, reply_len)) return false;
|
||||
strcpy(credentials.ssid, value);
|
||||
const bool ok = mesh::wifi::writeCredentials(credentials);
|
||||
snprintf(reply, reply_len, ok ? "OK - WiFi SSID saved; reboot to apply"
|
||||
: "Error: failed to save WiFi SSID");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool WebConfigServer::setStandaloneWiFiPassword(const char* value, char* reply,
|
||||
size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || !reply_len) return false;
|
||||
if (!mesh::cli::standaloneWiFiPasswordValid(value)) {
|
||||
snprintf(reply, reply_len,
|
||||
"Error: WiFi password must be 0-63 characters or 64 hex characters");
|
||||
snprintf(reply, reply_len, "Error: WiFi password must be 0-63 characters or 64 hex characters");
|
||||
return false;
|
||||
}
|
||||
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) {
|
||||
snprintf(reply, reply_len, "Error: failed to open WiFi settings");
|
||||
return false;
|
||||
}
|
||||
nvs.putString("password", value);
|
||||
const bool ok = nvs.getString("password", "\x01") == value;
|
||||
nvs.end();
|
||||
snprintf(reply, reply_len, ok ? "OK - WiFi password saved"
|
||||
mesh::wifi::Credentials credentials;
|
||||
if (!loadWiFiEdit(credentials, legacy_prefs, reply, reply_len)) return false;
|
||||
strcpy(credentials.password, value);
|
||||
const bool ok = mesh::wifi::writeCredentials(credentials);
|
||||
snprintf(reply, reply_len, ok ? "OK - WiFi password saved; reboot to apply"
|
||||
: "Error: failed to save WiFi password");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool WebConfigServer::setStandaloneWiFiPowerSave(const char* value, char* reply,
|
||||
size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || !reply_len) return false;
|
||||
uint8_t power_save = mesh::wifi::kDefaultPowerSave;
|
||||
if (!mesh::cli::parseStandaloneWiFiPowerSave(value, power_save)) {
|
||||
snprintf(reply, reply_len, "Error: power save must be none, min, or max");
|
||||
return false;
|
||||
}
|
||||
if (mesh::wifi::kPrimaryEspNowRadio
|
||||
&& power_save == mesh::wifi::kPowerSaveMax) {
|
||||
snprintf(reply, reply_len,
|
||||
"Error: power save max is unavailable while ESP-NOW is the primary radio");
|
||||
if (mesh::wifi::kPrimaryEspNowRadio && power_save == mesh::wifi::kPowerSaveMax) {
|
||||
snprintf(reply, reply_len, "Error: power save max is unavailable while ESP-NOW is the primary radio");
|
||||
return false;
|
||||
}
|
||||
if (effectiveWiFiPowerSave(power_save) != power_save) {
|
||||
snprintf(reply, reply_len,
|
||||
"Error: power save none is unavailable while Bluetooth is active");
|
||||
snprintf(reply, reply_len, "Error: power save none is unavailable while Bluetooth is active");
|
||||
return false;
|
||||
}
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) {
|
||||
snprintf(reply, reply_len, "Error: failed to open WiFi settings");
|
||||
return false;
|
||||
}
|
||||
const bool ok =
|
||||
nvs.putUChar("powersave", power_save) == sizeof(uint8_t);
|
||||
nvs.end();
|
||||
if (!ok) {
|
||||
mesh::wifi::Credentials credentials;
|
||||
if (!loadWiFiEdit(credentials, legacy_prefs, reply, reply_len)) return false;
|
||||
credentials.power_save = power_save;
|
||||
if (!mesh::wifi::writeCredentials(credentials)) {
|
||||
snprintf(reply, reply_len, "Error: failed to save WiFi power save");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t apply_result = ESP_OK;
|
||||
if (WiFi.getMode() != WIFI_OFF) {
|
||||
const wifi_ps_type_t ps_mode =
|
||||
power_save == mesh::wifi::kPowerSaveNone ? WIFI_PS_NONE
|
||||
: power_save == mesh::wifi::kPowerSaveMax ? WIFI_PS_MAX_MODEM
|
||||
: WIFI_PS_MIN_MODEM;
|
||||
apply_result = esp_wifi_set_ps(ps_mode);
|
||||
}
|
||||
if (apply_result == ESP_OK) {
|
||||
snprintf(reply, reply_len, "OK - WiFi power save set to %s", value);
|
||||
} else {
|
||||
snprintf(reply, reply_len,
|
||||
"OK - saved; WiFi power save applies on next connection");
|
||||
const wifi_ps_type_t mode = power_save == mesh::wifi::kPowerSaveNone ? WIFI_PS_NONE
|
||||
: power_save == mesh::wifi::kPowerSaveMax ? WIFI_PS_MAX_MODEM : WIFI_PS_MIN_MODEM;
|
||||
esp_wifi_set_ps(mode);
|
||||
}
|
||||
snprintf(reply, reply_len, "OK - WiFi power save saved; reboot to apply");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -578,34 +551,37 @@ bool WebConfigServer::setWiFiCliEnabled(const char* value, char* reply,
|
||||
bool WebConfigServer::reloadStandaloneWiFi() {
|
||||
return loadStandaloneWiFi(
|
||||
_wifi_ssid, sizeof(_wifi_ssid),
|
||||
_wifi_password, sizeof(_wifi_password), &_wifi_power_save);
|
||||
_wifi_password, sizeof(_wifi_password), &_wifi_power_save, _mqtt_prefs);
|
||||
}
|
||||
|
||||
bool WebConfigServer::formatWiFiSSID(char* reply, size_t reply_len) {
|
||||
bool WebConfigServer::formatWiFiSSID(char* reply, size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
|
||||
char ssid[32] = "";
|
||||
char password[65] = "";
|
||||
uint8_t power_save = mesh::wifi::kDefaultPowerSave;
|
||||
bool configured = loadStandaloneWiFi(
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save);
|
||||
if (_active && _active->_wifi_ssid[0]) {
|
||||
strncpy(ssid, _active->_wifi_ssid, sizeof(ssid) - 1);
|
||||
ssid[sizeof(ssid) - 1] = 0;
|
||||
configured = true;
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save, legacy_prefs);
|
||||
if (!legacy_prefs && _active && _active->_wifi_ssid[0]) {
|
||||
mesh::wifi::Credentials canonical;
|
||||
if (mesh::wifi::readCredentials(canonical) == mesh::wifi::CredentialState::Absent) {
|
||||
strlcpy(ssid, _active->_wifi_ssid, sizeof(ssid));
|
||||
configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(reply, reply_len, configured ? "> %s" : "> (not configured)", ssid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebConfigServer::formatWiFiPassword(char* reply, size_t reply_len) {
|
||||
bool WebConfigServer::formatWiFiPassword(char* reply, size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || !reply_len) return false;
|
||||
char ssid[33] = {}, password[65] = {};
|
||||
if (!loadStandaloneWiFi(ssid, sizeof(ssid), password, sizeof(password))) {
|
||||
if (_active) {
|
||||
strlcpy(password, _active->_wifi_password, sizeof(password));
|
||||
}
|
||||
char password[65] = {};
|
||||
mesh::wifi::Credentials credentials;
|
||||
const auto state = resolveWiFi(credentials, legacy_prefs);
|
||||
memcpy(password, credentials.password, sizeof(password));
|
||||
if (!legacy_prefs && state == mesh::wifi::CredentialState::Absent) {
|
||||
if (_active) strlcpy(password, _active->_wifi_password, sizeof(password));
|
||||
#ifdef WIFI_PWD
|
||||
else strlcpy(password, WIFI_PWD, sizeof(password));
|
||||
#endif
|
||||
@@ -615,25 +591,13 @@ bool WebConfigServer::formatWiFiPassword(char* reply, size_t reply_len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebConfigServer::formatWiFiPowerSave(char* reply, size_t reply_len) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
|
||||
uint8_t power_save = mesh::wifi::kDefaultPowerSave;
|
||||
if (_active) {
|
||||
power_save = _active->_wifi_power_save;
|
||||
} else {
|
||||
char ssid[32] = "";
|
||||
char password[65] = "";
|
||||
loadStandaloneWiFi(
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save);
|
||||
}
|
||||
|
||||
const char* name = "none";
|
||||
if (power_save == mesh::wifi::kPowerSaveMin) {
|
||||
name = "min";
|
||||
} else if (power_save == mesh::wifi::kPowerSaveMax) {
|
||||
name = "max";
|
||||
}
|
||||
bool WebConfigServer::formatWiFiPowerSave(char* reply, size_t reply_len, const void* legacy_prefs) {
|
||||
if (!reply || !reply_len) return false;
|
||||
mesh::wifi::Credentials credentials;
|
||||
resolveWiFi(credentials, legacy_prefs);
|
||||
const uint8_t power_save = effectiveWiFiPowerSave(credentials.power_save);
|
||||
const char* name = power_save == mesh::wifi::kPowerSaveMin ? "min"
|
||||
: power_save == mesh::wifi::kPowerSaveMax ? "max" : "none";
|
||||
snprintf(reply, reply_len, "> %s", name);
|
||||
return true;
|
||||
}
|
||||
@@ -657,19 +621,22 @@ bool WebConfigServer::formatWiFiCliStatus(char* reply, size_t reply_len) {
|
||||
|
||||
bool WebConfigServer::formatWiFiStatus(
|
||||
char* reply, size_t reply_len,
|
||||
const mesh::wifi::CompanionWiFiRuntimeState* companion_runtime) {
|
||||
const mesh::wifi::CompanionWiFiRuntimeState* companion_runtime,
|
||||
const void* legacy_prefs) {
|
||||
if (!reply || reply_len == 0) return false;
|
||||
|
||||
char ssid[32] = "";
|
||||
char password[65] = "";
|
||||
uint8_t power_save = mesh::wifi::kDefaultPowerSave;
|
||||
bool configured = loadStandaloneWiFi(
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save);
|
||||
ssid, sizeof(ssid), password, sizeof(password), &power_save, legacy_prefs);
|
||||
WebConfigServer* active = _active;
|
||||
if (active && active->_wifi_ssid[0]) {
|
||||
strncpy(ssid, active->_wifi_ssid, sizeof(ssid) - 1);
|
||||
ssid[sizeof(ssid) - 1] = 0;
|
||||
configured = true;
|
||||
if (!legacy_prefs && active && active->_wifi_ssid[0]) {
|
||||
mesh::wifi::Credentials canonical;
|
||||
if (mesh::wifi::readCredentials(canonical) == mesh::wifi::CredentialState::Absent) {
|
||||
strlcpy(ssid, active->_wifi_ssid, sizeof(ssid));
|
||||
configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (active && active->_mode == MODE_SETUP) {
|
||||
@@ -1533,6 +1500,9 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
if (_standalone_wifi_dirty) {
|
||||
_standalone_wifi_dirty = false;
|
||||
if (!saveStandaloneWiFi(_wifi_ssid, _wifi_password, _wifi_power_save)) {
|
||||
// Cached edits must not be used by a later portal station attempt after
|
||||
// a failed save. Reload the committed pair, or clear a blocked partial one.
|
||||
reloadStandaloneWiFi();
|
||||
// Attribute the persistence failure to the last WiFi field so it is
|
||||
// visible beside a concrete input in the UI.
|
||||
for (int i = _batch_count - 1; i >= 0; i--) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
// Shared browser configuration portal for ESP32 infrastructure and WiFi
|
||||
// companion builds. The UI removes MQTT controls when no bridge is present.
|
||||
//
|
||||
@@ -24,6 +25,8 @@
|
||||
|
||||
#define WITH_WEBCONFIG 1
|
||||
|
||||
#include <helpers/esp32/WiFiCredentials.h>
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
@@ -201,23 +204,27 @@ public:
|
||||
static bool saveCliEnabled(bool enabled);
|
||||
static bool loadStandaloneWiFi(char* ssid, size_t ssid_len,
|
||||
char* password, size_t password_len,
|
||||
uint8_t* power_save = NULL);
|
||||
uint8_t* power_save = NULL, const void* legacy_prefs = NULL);
|
||||
static bool hasConfiguredWiFi(const void* legacy_prefs = NULL);
|
||||
static mesh::wifi::CredentialState resolveWiFi(mesh::wifi::Credentials& out,
|
||||
const void* legacy_prefs = NULL);
|
||||
static bool saveStandaloneWiFi(const char* ssid, const char* password,
|
||||
uint8_t power_save);
|
||||
static bool setStandaloneWiFiSSID(const char* value, char* reply,
|
||||
size_t reply_len);
|
||||
size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool setStandaloneWiFiPassword(const char* value, char* reply,
|
||||
size_t reply_len);
|
||||
size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool setStandaloneWiFiPowerSave(const char* value, char* reply,
|
||||
size_t reply_len);
|
||||
size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool setWiFiCliEnabled(const char* value, char* reply,
|
||||
size_t reply_len);
|
||||
static bool formatWiFiSSID(char* reply, size_t reply_len);
|
||||
static bool formatWiFiPassword(char* reply, size_t reply_len);
|
||||
static bool formatWiFiSSID(char* reply, size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool formatWiFiPassword(char* reply, size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool formatWiFiStatus(
|
||||
char* reply, size_t reply_len,
|
||||
const mesh::wifi::CompanionWiFiRuntimeState* companion_runtime = NULL);
|
||||
static bool formatWiFiPowerSave(char* reply, size_t reply_len);
|
||||
const mesh::wifi::CompanionWiFiRuntimeState* companion_runtime = NULL,
|
||||
const void* legacy_prefs = NULL);
|
||||
static bool formatWiFiPowerSave(char* reply, size_t reply_len, const void* legacy_prefs = NULL);
|
||||
static bool formatWiFiCliStatus(char* reply, size_t reply_len);
|
||||
|
||||
// UI tasks use an otherwise-unused multi-click gesture without reaching into
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <Preferences.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/WiFiPowerSave.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace wifi {
|
||||
|
||||
// Read/write/resolve are loop-task-only, like CLI and queued portal writes.
|
||||
// MQTT Core 0 uses a separate snapshot; it must never call this NVS API.
|
||||
// Runtime only: the fixed /mqtt_prefs password field must remain 64 bytes.
|
||||
struct Credentials {
|
||||
char ssid[32] = {};
|
||||
char password[65] = {};
|
||||
uint8_t power_save = kDefaultPowerSave;
|
||||
};
|
||||
|
||||
enum class CredentialState { Absent, Unconfigured, Ready, Invalid, Unavailable };
|
||||
|
||||
inline CredentialState readCredentials(Credentials& out) {
|
||||
out = Credentials{};
|
||||
Preferences nvs;
|
||||
// Missing keys are normal on a fresh device; avoid Preferences NOT_FOUND logs.
|
||||
if (!nvs.begin("mesh-wifi", false)) return CredentialState::Unavailable;
|
||||
const bool ssid_present = nvs.isKey("ssid");
|
||||
const bool password_present = nvs.isKey("password");
|
||||
const bool ps_present = nvs.isKey("powersave");
|
||||
const bool pending_present = nvs.isKey("pending");
|
||||
bool valid = (!ssid_present || nvs.getType("ssid") == PT_STR)
|
||||
&& (!password_present || nvs.getType("password") == PT_STR)
|
||||
&& (!ps_present || nvs.getType("powersave") == PT_U8)
|
||||
&& (!pending_present || (nvs.getType("pending") == PT_U8
|
||||
&& nvs.getUChar("pending", 1) == 0));
|
||||
const String ssid = ssid_present && valid ? nvs.getString("ssid", "") : String();
|
||||
const String password = password_present && valid ? nvs.getString("password", "") : String();
|
||||
out.power_save = ps_present && valid
|
||||
? nvs.getUChar("powersave", kDefaultPowerSave) : kDefaultPowerSave;
|
||||
// Readers reject any write still in progress, including one that began
|
||||
// after the initial marker read. Firmware writers/readers run on the loop;
|
||||
// the MQTT worker only sees its frozen snapshot.
|
||||
valid = valid && (!nvs.isKey("pending")
|
||||
|| (nvs.getType("pending") == PT_U8 && nvs.getUChar("pending", 1) == 0));
|
||||
nvs.end();
|
||||
valid = valid && ssid.length() < sizeof(out.ssid)
|
||||
&& password.length() < sizeof(out.password)
|
||||
&& cli::standaloneWiFiPasswordValid(password.c_str())
|
||||
&& out.power_save <= kPowerSaveMax;
|
||||
if (!valid) { out = Credentials{}; return CredentialState::Invalid; }
|
||||
memcpy(out.ssid, ssid.c_str(), ssid.length() + 1);
|
||||
memcpy(out.password, password.c_str(), password.length() + 1);
|
||||
if (!ssid_present && !password_present) return CredentialState::Absent;
|
||||
// Historical SSID-only saves describe an open network. Password-only/empty
|
||||
// configurations never revive old credentials; interrupted writes are pending.
|
||||
return ssid_present && out.ssid[0]
|
||||
? CredentialState::Ready : CredentialState::Unconfigured;
|
||||
}
|
||||
|
||||
inline CredentialState resolveCredentials(Credentials& out,
|
||||
const char* legacy_ssid = nullptr, const char* legacy_password = nullptr,
|
||||
uint8_t legacy_power_save = kDefaultPowerSave) {
|
||||
const CredentialState state = readCredentials(out);
|
||||
if (state != CredentialState::Absent) return state;
|
||||
const uint8_t canonical_ps = out.power_save;
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) return CredentialState::Unavailable;
|
||||
const bool canonical_power_save = nvs.isKey("powersave");
|
||||
nvs.end();
|
||||
if (!legacy_ssid || !legacy_password) return state;
|
||||
const size_t ssid_len = strnlen(legacy_ssid, 32);
|
||||
const size_t password_len = strnlen(legacy_password, 64);
|
||||
if (ssid_len >= 32 || password_len >= 64
|
||||
|| !cli::standaloneWiFiPasswordValid(legacy_password)) {
|
||||
out = Credentials{};
|
||||
return CredentialState::Invalid;
|
||||
}
|
||||
memcpy(out.ssid, legacy_ssid, ssid_len + 1);
|
||||
memcpy(out.password, legacy_password, password_len + 1);
|
||||
out.power_save = canonical_power_save ? canonical_ps
|
||||
: (legacy_power_save <= kPowerSaveMax ? legacy_power_save : kDefaultPowerSave);
|
||||
return ssid_len ? CredentialState::Ready : CredentialState::Unconfigured;
|
||||
}
|
||||
|
||||
inline bool writeCredentials(const Credentials& candidate) {
|
||||
if (strnlen(candidate.ssid, sizeof(candidate.ssid)) >= sizeof(candidate.ssid)
|
||||
|| strnlen(candidate.password, sizeof(candidate.password)) >= sizeof(candidate.password)
|
||||
|| !cli::standaloneWiFiPasswordValid(candidate.password)
|
||||
|| candidate.power_save > kPowerSaveMax) return false;
|
||||
Credentials previous;
|
||||
const CredentialState previous_state = readCredentials(previous);
|
||||
if (previous_state == CredentialState::Unavailable) return false;
|
||||
Preferences nvs;
|
||||
if (!nvs.begin("mesh-wifi", false)) return false;
|
||||
const bool had_ssid = nvs.isKey("ssid"), had_password = nvs.isKey("password");
|
||||
const bool had_ps = nvs.isKey("powersave");
|
||||
// Corrupt/held settings may be repaired by a complete explicit portal save.
|
||||
const String old_ssid = had_ssid ? nvs.getString("ssid", "") : String();
|
||||
const String old_password = had_password ? nvs.getString("password", "") : String();
|
||||
const uint8_t old_ps = had_ps ? nvs.getUChar("powersave", kDefaultPowerSave) : kDefaultPowerSave;
|
||||
// Fail closed if power is lost between independent NVS key writes.
|
||||
if (nvs.putUChar("pending", 1) != 1) { nvs.end(); return false; }
|
||||
nvs.putString("ssid", candidate.ssid);
|
||||
nvs.putString("password", candidate.password); // empty string can return zero
|
||||
nvs.putUChar("powersave", candidate.power_save);
|
||||
bool ok = nvs.getType("ssid") == PT_STR && nvs.getType("password") == PT_STR
|
||||
&& nvs.getType("powersave") == PT_U8
|
||||
&& nvs.getString("ssid", "\x01") == candidate.ssid
|
||||
&& nvs.getString("password", "\x01") == candidate.password
|
||||
&& nvs.getUChar("powersave", 255) == candidate.power_save;
|
||||
if (ok) ok = nvs.putUChar("pending", 0) == 1
|
||||
&& nvs.getUChar("pending", 1) == 0;
|
||||
if (!ok) {
|
||||
// A failed commit can have changed the visible marker. Restore the barrier
|
||||
// before touching any rollback fields, or leave the complete candidate.
|
||||
const bool blocked = nvs.putUChar("pending", 1) == 1
|
||||
&& nvs.getUChar("pending", 0) == 1;
|
||||
if (!blocked) { nvs.end(); return false; }
|
||||
if (had_ssid) nvs.putString("ssid", old_ssid); else nvs.remove("ssid");
|
||||
if (had_password) nvs.putString("password", old_password); else nvs.remove("password");
|
||||
if (had_ps) nvs.putUChar("powersave", old_ps); else nvs.remove("powersave");
|
||||
const bool restored = nvs.isKey("ssid") == had_ssid
|
||||
&& nvs.isKey("password") == had_password && nvs.isKey("powersave") == had_ps
|
||||
&& (!had_ssid || nvs.getString("ssid", "\x01") == old_ssid)
|
||||
&& (!had_password || nvs.getString("password", "\x01") == old_password)
|
||||
&& (!had_ps || nvs.getUChar("powersave", 255) == old_ps);
|
||||
if (restored && previous_state != CredentialState::Invalid) nvs.putUChar("pending", 0);
|
||||
// Otherwise leave pending=1: no consumer may use a partially saved pair.
|
||||
}
|
||||
nvs.end();
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace mesh
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "helpers/esp32/WiFiCredentials.h"
|
||||
#include "helpers/MQTTPrefsStorage.h"
|
||||
|
||||
using mesh::wifi::CredentialState;
|
||||
using mesh::wifi::Credentials;
|
||||
namespace mesh { namespace wifi {
|
||||
constexpr bool kPrimaryEspNowRadio = false;
|
||||
std::string joined_ssid, joined_password;
|
||||
int joins = 0;
|
||||
void beginStation(const char* ssid, const char* password) {
|
||||
joined_ssid = ssid; joined_password = password; ++joins;
|
||||
}
|
||||
}}
|
||||
uint8_t effectiveWiFiPowerSave(uint8_t value) { return value; }
|
||||
enum { WIFI_OFF, WIFI_STA, WIFI_PS_NONE, WIFI_PS_MAX_MODEM, WIFI_PS_MIN_MODEM };
|
||||
struct { int getMode() { return WIFI_STA; } } WiFi;
|
||||
using wifi_ps_type_t = int;
|
||||
int esp_wifi_set_ps(int) { return 0; }
|
||||
|
||||
size_t strlcpy(char* to, const char* from, size_t size) {
|
||||
const size_t len = strlen(from);
|
||||
if (size) { memcpy(to, from, len < size ? len + 1 : size - 1); to[size - 1] = 0; }
|
||||
return len;
|
||||
}
|
||||
|
||||
struct WebConfigServer {
|
||||
inline static WebConfigServer* _active = nullptr;
|
||||
char _wifi_ssid[32] = {}, _wifi_password[65] = {};
|
||||
uint8_t _wifi_power_save = mesh::wifi::kDefaultPowerSave;
|
||||
void* _mqtt_prefs = nullptr;
|
||||
bool _standalone_wifi = true, _standalone_wifi_dirty = false, _batch_all_ok = true;
|
||||
struct Entry { const char* key; char reply[160] = {}; };
|
||||
Entry _batch[3] = {{"wifi.ssid"}, {"wifi.pwd"}, {"wifi.powersave"}};
|
||||
int _batch_count = 3;
|
||||
bool bluetoothWiFiCoexistenceRequired() { return false; }
|
||||
bool reloadStandaloneWiFi();
|
||||
bool acceptWiFi(Entry& e, const char* value) {
|
||||
@CREDENTIAL_BATCH@
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool commitWiFi() {
|
||||
@COMMIT_BATCH@
|
||||
return _batch_all_ok;
|
||||
}
|
||||
static CredentialState resolveWiFi(Credentials&, const void* = nullptr);
|
||||
static bool hasConfiguredWiFi(const void* = nullptr);
|
||||
static bool loadStandaloneWiFi(char*, size_t, char*, size_t, uint8_t* = nullptr,
|
||||
const void* = nullptr);
|
||||
static bool saveStandaloneWiFi(const char*, const char*, uint8_t);
|
||||
static bool setStandaloneWiFiSSID(const char*, char*, size_t, const void* = nullptr);
|
||||
static bool setStandaloneWiFiPassword(const char*, char*, size_t, const void* = nullptr);
|
||||
static bool setStandaloneWiFiPowerSave(const char*, char*, size_t, const void* = nullptr);
|
||||
static bool formatWiFiPassword(char*, size_t, const void* = nullptr);
|
||||
};
|
||||
@WEB_METHODS@
|
||||
|
||||
struct MQTTNodeInfo { bool canonical_wifi = false; };
|
||||
constexpr int RUNTIME_MQTT_SLOTS = 2;
|
||||
constexpr const char* MQTT_PRESET_NONE = "none";
|
||||
constexpr const char* MQTT_PRESET_CUSTOM = "custom";
|
||||
const void* findMQTTPreset(const char* name) {
|
||||
return strcmp(name, "test") == 0 ? reinterpret_cast<void*>(1) : nullptr;
|
||||
}
|
||||
#define MQTT_DEBUG_PRINTLN(...) {}
|
||||
static bool isWiFiConfigValid(const MQTTPrefs*, bool);
|
||||
struct MQTTBridge {
|
||||
MQTTPrefs* _obs;
|
||||
MQTTNodeInfo _node_info;
|
||||
bool _manage_wifi = true, _wifi_configured = false, _initialized = false;
|
||||
char _wifi_ssid[32] = {}, _wifi_password[65] = {};
|
||||
uint8_t _wifi_power_save = 0;
|
||||
std::atomic<bool> _stop_requested{false};
|
||||
explicit MQTTBridge(MQTTPrefs& obs, bool canonical = true, bool managed = true)
|
||||
: _obs(&obs), _node_info{canonical}, _manage_wifi(managed) {}
|
||||
bool prepareWiFiCredentials();
|
||||
void beginWiFiStation();
|
||||
bool isReady() const;
|
||||
static bool isConfigValid(const MQTTPrefs*, bool = false);
|
||||
void begin() {
|
||||
@BEGIN_GATE@
|
||||
_initialized = true;
|
||||
if (_manage_wifi) beginWiFiStation();
|
||||
}
|
||||
void reconnect() {
|
||||
@RECONNECT@
|
||||
}
|
||||
};
|
||||
@MQTT_METHODS@
|
||||
|
||||
struct CmdCallbacks {
|
||||
MQTTPrefs* legacy;
|
||||
bool canonical = true;
|
||||
bool usesCanonicalWiFi() const { return canonical; }
|
||||
bool setWiFiSSID(const char* value, char* reply) { return WebConfigServer::setStandaloneWiFiSSID(value, reply, 160, legacy); }
|
||||
bool setWiFiPassword(const char* value, char* reply) { return WebConfigServer::setStandaloneWiFiPassword(value, reply, 160, legacy); }
|
||||
bool setWiFiPowerSave(const char* value, char* reply) { return WebConfigServer::setStandaloneWiFiPowerSave(value, reply, 160, legacy); }
|
||||
bool getWiFiPassword(char* reply) { return WebConfigServer::formatWiFiPassword(reply, 160, legacy); }
|
||||
bool getWiFiSSID(char* reply) {
|
||||
Credentials credentials; WebConfigServer::resolveWiFi(credentials, legacy);
|
||||
snprintf(reply, 160, "> %s", credentials.ssid); return true;
|
||||
}
|
||||
bool getWiFiStatus(char* reply) { strcpy(reply, "status"); return true; }
|
||||
bool getWiFiPowerSave(char* reply) { strcpy(reply, "power save"); return true; }
|
||||
};
|
||||
struct CommonCLI {
|
||||
CmdCallbacks* _callbacks;
|
||||
bool handleObserverSetCmd(uint32_t, const char* config, char* reply) {
|
||||
@CLI_SET@
|
||||
return false; // untouched observer fallback follows this production block
|
||||
}
|
||||
bool handleObserverGetCmd(uint32_t sender_timestamp, const char* config, char* reply) {
|
||||
@CLI_GET@
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
void assert_selected(MQTTBridge& bridge, const char* ssid, const char* password) {
|
||||
assert(bridge.isReady());
|
||||
assert(std::string(bridge._wifi_ssid) == ssid);
|
||||
assert(std::string(bridge._wifi_password) == password);
|
||||
const int old_joins = mesh::wifi::joins;
|
||||
bridge.reconnect();
|
||||
assert(mesh::wifi::joins == old_joins + 1);
|
||||
assert(mesh::wifi::joined_ssid == ssid && mesh::wifi::joined_password == password);
|
||||
const int opens = nvs_opens;
|
||||
for (int i = 0; i < 100; ++i) assert(bridge.isReady());
|
||||
assert(nvs_opens == opens); // no NVS/String operations on the hot path
|
||||
}
|
||||
|
||||
int main() {
|
||||
static_assert(sizeof(MQTTPrefs::wifi_password) == 64, "persistent codec unchanged");
|
||||
MQTTPrefs obs{};
|
||||
strcpy(obs.mqtt_slot_preset[0], "test");
|
||||
const MQTTPrefs untouched = obs;
|
||||
char reply[160] = {}, ssid[32] = {};
|
||||
reset_nvs();
|
||||
|
||||
// Fresh Full portal-only provisioning; the frozen observer store is empty.
|
||||
assert(!MQTTBridge::isConfigValid(&obs, true));
|
||||
MQTTBridge empty(obs); empty.begin(); assert(!empty.isReady());
|
||||
WebConfigServer fresh; fresh._mqtt_prefs = &obs;
|
||||
assert(fresh.acceptWiFi(fresh._batch[0], "portal"));
|
||||
assert(fresh.acceptWiFi(fresh._batch[1], "password"));
|
||||
assert(fresh.acceptWiFi(fresh._batch[2], "max"));
|
||||
assert(fresh.commitWiFi());
|
||||
assert(memcmp(&obs, &untouched, sizeof(obs)) == 0);
|
||||
assert(WebConfigServer::hasConfiguredWiFi(&obs));
|
||||
assert(WebConfigServer::loadStandaloneWiFi(ssid, sizeof(ssid), nullptr, 0));
|
||||
assert(std::string(ssid) == "portal");
|
||||
assert(MQTTBridge::isConfigValid(&obs, true));
|
||||
assert(!MQTTBridge::isConfigValid(&obs, false));
|
||||
MQTTBridge portal(obs); portal.begin();
|
||||
assert(portal._wifi_power_save == 2); assert_selected(portal, "portal", "password");
|
||||
|
||||
// Saved changes affect a new boot/restart, never a live worker snapshot.
|
||||
assert(WebConfigServer::saveStandaloneWiFi("changed", "new-password", 0));
|
||||
assert_selected(portal, "portal", "password");
|
||||
MQTTBridge reboot(obs); reboot.begin();
|
||||
assert(reboot._wifi_power_save == 0); assert_selected(reboot, "changed", "new-password");
|
||||
std::string raw(64, 'A');
|
||||
assert(WebConfigServer::saveStandaloneWiFi("raw", raw.c_str(), 1));
|
||||
MQTTBridge raw_bridge(obs); raw_bridge.begin(); assert_selected(raw_bridge, "raw", raw.c_str());
|
||||
assert(WebConfigServer::formatWiFiPassword(reply, sizeof(reply), &obs));
|
||||
assert(std::string(reply) == "> " + raw);
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("bad", std::string(64, 'Z').c_str(), 1));
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("bad", std::string(65, 'A').c_str(), 1));
|
||||
assert(!WebConfigServer::saveStandaloneWiFi(std::string(32, 'a').c_str(), "", 1));
|
||||
MQTTBridge unchanged(obs); unchanged.begin(); assert_selected(unchanged, "raw", raw.c_str());
|
||||
|
||||
// Production command routing must reach the canonical callbacks before the
|
||||
// legacy observer handler, retain remote secret masking, and consume errors.
|
||||
CmdCallbacks callbacks{&obs}; CommonCLI cli{&callbacks};
|
||||
assert(cli.handleObserverSetCmd(0, "wifi.ssid routed", reply));
|
||||
const std::string raw_command = "wifi.pwd " + raw;
|
||||
assert(cli.handleObserverSetCmd(0, raw_command.c_str(), reply));
|
||||
assert(cli.handleObserverGetCmd(0, "wifi.pwd", reply));
|
||||
assert(std::string(reply) == "> " + raw);
|
||||
assert(cli.handleObserverGetCmd(1, "wifi.pwd", reply));
|
||||
assert(std::string(reply) == "> ******** (local connection only)");
|
||||
assert(cli.handleObserverSetCmd(0, "wifi.powersave min", reply));
|
||||
assert(cli.handleObserverSetCmd(0, "wifi.pwd invalid-64", reply));
|
||||
assert(memcmp(&obs, &untouched, sizeof(obs)) == 0);
|
||||
callbacks.canonical = false;
|
||||
assert(!cli.handleObserverSetCmd(0, "wifi.ssid legacy-route", reply));
|
||||
assert(!cli.handleObserverGetCmd(0, "wifi.pwd", reply));
|
||||
|
||||
// Older observer-only installs fall back, and first CLI write promotes a pair.
|
||||
reset_nvs(); strcpy(obs.wifi_ssid, "legacy"); strcpy(obs.wifi_password, "legacy-password");
|
||||
obs.wifi_power_save = 2;
|
||||
MQTTBridge legacy(obs); legacy.begin(); assert_selected(legacy, "legacy", "legacy-password");
|
||||
const MQTTPrefs legacy_untouched = obs;
|
||||
assert(WebConfigServer::setStandaloneWiFiSSID("promoted", reply, sizeof(reply), &obs));
|
||||
assert(memcmp(&obs, &legacy_untouched, sizeof(obs)) == 0);
|
||||
MQTTBridge promoted(obs); promoted.begin(); assert_selected(promoted, "promoted", "legacy-password");
|
||||
reset_nvs();
|
||||
assert(WebConfigServer::setStandaloneWiFiPassword(raw.c_str(), reply, sizeof(reply), &obs));
|
||||
MQTTBridge password_first(obs); password_first.begin(); assert_selected(password_first, "legacy", raw.c_str());
|
||||
|
||||
// Fresh password-first and SSID-first CLI setup, including open networks.
|
||||
reset_nvs(); obs.wifi_ssid[0] = obs.wifi_password[0] = 0;
|
||||
assert(WebConfigServer::setStandaloneWiFiPassword("first-password", reply, sizeof(reply), &obs));
|
||||
assert(!WebConfigServer::hasConfiguredWiFi(&obs));
|
||||
assert(WebConfigServer::setStandaloneWiFiSSID("first-ssid", reply, sizeof(reply), &obs));
|
||||
MQTTBridge first(obs); first.begin(); assert_selected(first, "first-ssid", "first-password");
|
||||
reset_nvs();
|
||||
assert(WebConfigServer::setStandaloneWiFiSSID("open", reply, sizeof(reply), &obs));
|
||||
MQTTBridge open(obs); open.begin(); assert_selected(open, "open", "");
|
||||
reset_nvs(); nvs_values["ssid"] = {PT_STR, "old-open", 0};
|
||||
MQTTBridge old_open(obs); old_open.begin(); assert_selected(old_open, "old-open", "");
|
||||
|
||||
// Clearing MQTT CLI SSID preserves its historical recovery command while
|
||||
// making the canonical empty value authoritative over old observer settings.
|
||||
assert(WebConfigServer::setStandaloneWiFiSSID("", reply, sizeof(reply), &obs));
|
||||
assert(!WebConfigServer::hasConfiguredWiFi(&obs));
|
||||
assert(!WebConfigServer::setStandaloneWiFiSSID("", reply, sizeof(reply)));
|
||||
|
||||
// Canonical empty, malformed, missing SSID, interrupted or inaccessible data
|
||||
// must never revive valid observer credentials.
|
||||
strcpy(obs.wifi_ssid, "legacy"); strcpy(obs.wifi_password, "legacy-password");
|
||||
for (int fault = 0; fault < 7; ++fault) {
|
||||
reset_nvs();
|
||||
nvs_values["ssid"] = {PT_STR, fault == 0 ? "" : "canonical", 0};
|
||||
nvs_values["password"] = {PT_STR, "pwd", 0};
|
||||
if (fault == 1) nvs_values["password"].string = std::string(64, 'Z');
|
||||
if (fault == 2) nvs_values["ssid"].string = std::string(32, 'a');
|
||||
if (fault == 3) nvs_values["ssid"].type = PT_U8;
|
||||
if (fault == 4) nvs_values.erase("ssid");
|
||||
if (fault == 5) nvs_values["pending"] = {PT_U8, {}, 1};
|
||||
if (fault == 6) nvs_available = false;
|
||||
assert(!MQTTBridge::isConfigValid(&obs, true));
|
||||
MQTTBridge broken(obs); broken.begin(); assert(!broken.isReady());
|
||||
assert(!WebConfigServer::hasConfiguredWiFi(&obs));
|
||||
}
|
||||
|
||||
// Companion-managed and default legacy/Pico semantics ignore infrastructure NVS.
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("infra", raw.c_str(), 1));
|
||||
MQTTBridge companion(obs, true, false); companion.begin();
|
||||
assert(std::string(companion._wifi_ssid) == "legacy");
|
||||
assert(std::string(companion._wifi_password) == "legacy-password");
|
||||
MQTTBridge legacy_role(obs, false); legacy_role.begin();
|
||||
assert_selected(legacy_role, "legacy", "legacy-password");
|
||||
|
||||
// Every individual failed NVS write rolls back a committed pair. Persistent
|
||||
// failures/interrupted saves stay closed; a failed repair cannot clear pending.
|
||||
for (int failed_write = 1; failed_write <= 5; ++failed_write) {
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("before", "before-pwd", 1));
|
||||
nvs_writes = 0; nvs_fail_write = failed_write;
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("after", "after-pwd", 2));
|
||||
nvs_fail_write = -1;
|
||||
MQTTBridge rolled_back(obs); rolled_back.begin();
|
||||
assert_selected(rolled_back, "before", "before-pwd");
|
||||
}
|
||||
// A failed portal batch reloads the committed cache; it cannot later join
|
||||
// the edited-but-unsaved tuple after its AP remains open.
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("before", "before-pwd", 1));
|
||||
WebConfigServer failed_portal; failed_portal._mqtt_prefs = &obs;
|
||||
assert(failed_portal.reloadStandaloneWiFi());
|
||||
assert(failed_portal.acceptWiFi(failed_portal._batch[0], "unsaved"));
|
||||
assert(failed_portal.acceptWiFi(failed_portal._batch[1], "unsaved-password"));
|
||||
nvs_writes = 0; nvs_fail_write = 3;
|
||||
assert(!failed_portal.commitWiFi());
|
||||
assert(std::string(failed_portal._wifi_ssid) == "before");
|
||||
assert(std::string(failed_portal._wifi_password) == "before-pwd");
|
||||
nvs_fail_write = -1;
|
||||
// A final marker commit may change the value before returning failure.
|
||||
// Rollback must re-establish its barrier; if that fails, leave the complete
|
||||
// candidate unchanged instead of exposing a mixed credential pair.
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("before", "before-pwd", 1));
|
||||
nvs_writes = 0; nvs_fail_write = nvs_side_effect_write = 5;
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("after", "after-pwd", 2));
|
||||
nvs_fail_write = nvs_side_effect_write = -1;
|
||||
MQTTBridge side_effect(obs); side_effect.begin(); assert_selected(side_effect, "before", "before-pwd");
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("before", "before-pwd", 1));
|
||||
nvs_writes = 0; nvs_fail_write = nvs_side_effect_write = 5; nvs_fail_write_again = 6;
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("after", "after-pwd", 2));
|
||||
nvs_fail_write = nvs_side_effect_write = nvs_fail_write_again = -1;
|
||||
MQTTBridge no_barrier(obs); no_barrier.begin(); assert_selected(no_barrier, "after", "after-pwd");
|
||||
reset_nvs(); assert(WebConfigServer::saveStandaloneWiFi("before", "before-pwd", 1));
|
||||
nvs_writes = 0; nvs_fail_write = 3; nvs_fail_forever = true;
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("after", "after-pwd", 2));
|
||||
nvs_fail_write = -1; nvs_fail_forever = false;
|
||||
MQTTBridge partial(obs); partial.begin(); assert(!partial.isReady());
|
||||
nvs_values["pending"] = {PT_U8, {}, 1};
|
||||
nvs_writes = 0; nvs_fail_write = 3;
|
||||
assert(!WebConfigServer::saveStandaloneWiFi("repair", "repair-pwd", 1));
|
||||
nvs_fail_write = -1;
|
||||
MQTTBridge failed_repair(obs); failed_repair.begin(); assert(!failed_repair.isReady());
|
||||
assert(WebConfigServer::saveStandaloneWiFi("repair", "repair-pwd", 1));
|
||||
MQTTBridge repair(obs); repair.begin(); assert_selected(repair, "repair", "repair-pwd");
|
||||
// No-canonical Companion reads retain compiled/live credentials, whereas
|
||||
// explicit invalid or empty canonical settings cannot revive those secrets.
|
||||
reset_nvs();
|
||||
#ifdef WIFI_PWD
|
||||
assert(WebConfigServer::formatWiFiPassword(reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> compiled-password");
|
||||
#endif
|
||||
WebConfigServer live;
|
||||
strcpy(live._wifi_password, "live-password"); WebConfigServer::_active = &live;
|
||||
assert(WebConfigServer::formatWiFiPassword(reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> live-password");
|
||||
nvs_values["ssid"] = {PT_STR, "", 0};
|
||||
nvs_values["password"] = {PT_STR, "", 0};
|
||||
assert(WebConfigServer::formatWiFiPassword(reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> ");
|
||||
nvs_values["pending"] = {PT_U8, {}, 1};
|
||||
assert(WebConfigServer::formatWiFiPassword(reply, sizeof(reply)));
|
||||
assert(std::string(reply) == "> ");
|
||||
WebConfigServer::_active = nullptr;
|
||||
|
||||
}
|
||||
+11
-2
@@ -103,8 +103,17 @@ public:
|
||||
WebConfigServer(Callbacks*, void*, bool, const uint8_t*, const char*,
|
||||
const char*, const char*, const char*, bool);
|
||||
static bool loadCliEnabled(bool value) { return value; }
|
||||
static bool loadStandaloneWiFi(char* ssid, size_t, char* pwd, size_t, uint8_t* ps) {
|
||||
if (!canonical_available) return false;
|
||||
static bool loadStandaloneWiFi(char* ssid, size_t, char* pwd, size_t, uint8_t* ps, const void* legacy = nullptr) {
|
||||
if (!canonical_available) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (legacy) {
|
||||
const auto* obs = static_cast<const MQTTPrefs*>(legacy);
|
||||
strcpy(ssid, obs->wifi_ssid); strcpy(pwd, obs->wifi_password);
|
||||
*ps = obs->wifi_power_save; return ssid[0] != 0;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
strcpy(ssid, canonical_ssid); strcpy(pwd, canonical_password.c_str()); *ps = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
struct String {
|
||||
std::string value;
|
||||
String() = default;
|
||||
String(const char* s) : value(s) {}
|
||||
String(const std::string& s) : value(s) {}
|
||||
size_t length() const { return value.size(); }
|
||||
const char* c_str() const { return value.c_str(); }
|
||||
bool operator==(const char* s) const { return value == s; }
|
||||
bool operator==(const String& s) const { return value == s.value; }
|
||||
};
|
||||
enum PreferenceType { PT_INVALID, PT_STR, PT_U8 };
|
||||
struct NvsValue { PreferenceType type; std::string string; uint8_t byte; };
|
||||
inline std::map<std::string, NvsValue> nvs_values;
|
||||
inline int nvs_writes = 0, nvs_fail_write = -1, nvs_opens = 0;
|
||||
inline int nvs_side_effect_write = -1, nvs_fail_write_again = -1;
|
||||
inline bool nvs_fail_forever = false, nvs_available = true;
|
||||
inline bool nvs_write_allowed() {
|
||||
++nvs_writes;
|
||||
if (nvs_writes == nvs_fail_write_again) return false;
|
||||
return nvs_fail_write < 0 || (nvs_fail_forever
|
||||
? nvs_writes < nvs_fail_write : nvs_writes != nvs_fail_write);
|
||||
}
|
||||
struct Preferences {
|
||||
bool begin(const char* name, bool) {
|
||||
assert(std::string(name) == "mesh-wifi"); ++nvs_opens; return nvs_available;
|
||||
}
|
||||
void end() {}
|
||||
bool isKey(const char* key) { return nvs_values.count(key) != 0; }
|
||||
PreferenceType getType(const char* key) {
|
||||
return isKey(key) ? nvs_values.at(key).type : PT_INVALID;
|
||||
}
|
||||
String getString(const char* key, const char* fallback) {
|
||||
return getType(key) == PT_STR ? String(nvs_values.at(key).string) : String(fallback);
|
||||
}
|
||||
uint8_t getUChar(const char* key, uint8_t fallback) {
|
||||
return getType(key) == PT_U8 ? nvs_values.at(key).byte : fallback;
|
||||
}
|
||||
size_t putString(const char* key, const String& value) {
|
||||
const bool ok = nvs_write_allowed();
|
||||
if (ok || nvs_writes == nvs_side_effect_write) nvs_values[key] = {PT_STR, value.value, 0};
|
||||
return ok ? value.length() : 0;
|
||||
}
|
||||
size_t putUChar(const char* key, uint8_t value) {
|
||||
const bool ok = nvs_write_allowed();
|
||||
if (ok || nvs_writes == nvs_side_effect_write) nvs_values[key] = {PT_U8, {}, value};
|
||||
return ok ? 1 : 0;
|
||||
}
|
||||
bool remove(const char* key) {
|
||||
if (!nvs_write_allowed()) return false;
|
||||
nvs_values.erase(key); return true;
|
||||
}
|
||||
};
|
||||
inline void reset_nvs() {
|
||||
nvs_values.clear(); nvs_writes = nvs_opens = 0; nvs_fail_write = -1;
|
||||
nvs_fail_forever = false; nvs_available = true;
|
||||
nvs_side_effect_write = nvs_fail_write_again = -1;
|
||||
}
|
||||
@@ -140,7 +140,7 @@ struct ESPNowBridge : AbstractBridge {
|
||||
};
|
||||
struct MQTTNodeInfo {
|
||||
const char* node_name; float* freq; float* bw;
|
||||
uint8_t* sf; uint8_t* cr; uint8_t* repeat_flag; bool repeat_when_nonzero;
|
||||
uint8_t* sf; uint8_t* cr; uint8_t* repeat_flag; bool repeat_when_nonzero; bool canonical_wifi = false;
|
||||
};
|
||||
struct MQTTBridge : AbstractBridge {
|
||||
template<class... Args> explicit MQTTBridge(Args...) {}
|
||||
@@ -161,6 +161,7 @@ struct WebConfigServer {
|
||||
if (prefs) strcpy(_wifi_ssid, static_cast<ObserverPrefs*>(prefs)->wifi_ssid);
|
||||
}
|
||||
static bool loadEnabled(bool) { return enabled; }
|
||||
static bool hasConfiguredWiFi(const void* prefs) { return static_cast<const ObserverPrefs*>(prefs)->wifi_ssid[0] != 0; }
|
||||
bool isRunning() const { return running; }
|
||||
bool isStopping() const { return stopping; }
|
||||
void updateWiFiOwnership(bool owner) { owns = owner; }
|
||||
|
||||
@@ -5,6 +5,9 @@ import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
import test_wifi_ota_start as wifi_start
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PROFILE = ROOT / "variants" / "heltec_v4" / "platformio.ini"
|
||||
@@ -21,6 +24,7 @@ def ini_section(source: str, name: str) -> str:
|
||||
|
||||
|
||||
class HeltecV4WiFiSetupPageTest(unittest.TestCase):
|
||||
compile_and_run = wifi_start.WiFiOtaStartTest.compile_and_run
|
||||
def test_only_main_v4_oled_full_profiles_enable_the_page(self):
|
||||
profile = PROFILE.read_text(encoding="utf-8")
|
||||
full_profiles = (
|
||||
@@ -84,22 +88,93 @@ class HeltecV4WiFiSetupPageTest(unittest.TestCase):
|
||||
|
||||
def test_short_click_leaves_page_and_hold_toggles_session_ap(self):
|
||||
ui = UI.read_text(encoding="utf-8")
|
||||
home_handler_start = ui.index(" bool handleInput(char c) override {")
|
||||
home_handler_end = ui.index("\n};", home_handler_start)
|
||||
handler = ui[home_handler_start:home_handler_end]
|
||||
|
||||
next_page = handler.index("if (c == KEY_NEXT || c == KEY_RIGHT)")
|
||||
setup_action = handler.index(
|
||||
"if (c == KEY_ENTER && _page == HomePage::WIFI_SETUP)"
|
||||
)
|
||||
self.assertLess(next_page, setup_action)
|
||||
self.assertIn("_page = (_page + 1) % HomePage::Count;", handler)
|
||||
|
||||
setup_handler = handler[setup_action:]
|
||||
self.assertIn("WebConfigServer::getSetupInfo(nullptr, 0, nullptr, 0)", setup_handler)
|
||||
self.assertIn("requestCompanionWiFiSetupStop();", setup_handler)
|
||||
self.assertIn("requestCompanionWiFiSetup();", setup_handler)
|
||||
self.assertNotIn("!isCompanionWiFiConnected()", setup_handler)
|
||||
home = ui[ui.index("class HomeScreen :") :]
|
||||
handler = extract_braced(home, " bool handleInput(char c) override")
|
||||
page_enum = extract_braced(home, " enum HomePage") + ";"
|
||||
navigation = extract_braced(home, " void movePage(int direction)")
|
||||
visible = extract_braced(home, " bool isPageVisible(uint8_t page) const")
|
||||
main = MAIN.read_text()
|
||||
requests = "\n".join(extract_braced(main, name) for name in (
|
||||
" void requestCompanionWiFiSetup()", " void requestCompanionWiFiSetupStop()"))
|
||||
# Execute production signed-char navigation and HOLD setup handling.
|
||||
# Rendering/radio endpoints are mocked; preferences saves are absent,
|
||||
# so an accidental permanent toggle cannot compile in this fixture.
|
||||
source = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <helpers/WirelessControl.h>
|
||||
#define UI_WIFI_SETUP_HOME_PAGE 1
|
||||
#define UI_NO_DISCOVER_SCREEN 1
|
||||
#define UI_NO_HIBERNATE 1
|
||||
#define KEY_LEFT 0xB4
|
||||
#define KEY_RIGHT 0xB7
|
||||
#define KEY_PREV 0xF2
|
||||
#define KEY_NEXT 0xF1
|
||||
#define KEY_ENTER 13
|
||||
bool companion_wifi_setup_requested = false, companion_wifi_setup_stop_requested = false;
|
||||
@REQUESTS@
|
||||
struct WebConfigServer {
|
||||
static bool active;
|
||||
static bool getSetupInfo(void*, int, void*, int) { return active; }
|
||||
};
|
||||
bool WebConfigServer::active = false;
|
||||
struct Mesh { bool isDualRadioActive() const { return false; } bool advert() { return true; } } the_mesh;
|
||||
enum class UIEventType { ack };
|
||||
struct UITask {
|
||||
void showAlert(const char*, int) {}
|
||||
bool isBluetoothEnabled() { return false; }
|
||||
void disableBluetooth() {} void enableBluetooth() {}
|
||||
void showMessages() {} void notify(UIEventType) {}
|
||||
};
|
||||
struct UIScreen { virtual bool handleInput(char) { return false; } };
|
||||
struct HomeScreen : UIScreen {
|
||||
@PAGES@
|
||||
int _page = WIFI_SETUP, _radio_status_page = 0;
|
||||
UITask* _task;
|
||||
explicit HomeScreen(UITask* task) : _task(task) {}
|
||||
void resetRadioProfileDisplayPage() {}
|
||||
@VISIBLE@
|
||||
@NAVIGATION@
|
||||
@HANDLER@
|
||||
};
|
||||
int main() {
|
||||
UITask task; HomeScreen home(&task);
|
||||
assert(home.handleInput(static_cast<char>(KEY_NEXT)));
|
||||
assert(home._page == HomeScreen::FIRST);
|
||||
assert(!companion_wifi_setup_requested && !companion_wifi_setup_stop_requested);
|
||||
assert(home.handleInput(static_cast<char>(KEY_PREV)));
|
||||
assert(home._page == HomeScreen::WIFI_SETUP);
|
||||
assert(home.handleInput(KEY_ENTER));
|
||||
assert(companion_wifi_setup_requested && !companion_wifi_setup_stop_requested);
|
||||
WebConfigServer::active = true;
|
||||
assert(home.handleInput(KEY_ENTER));
|
||||
assert(!companion_wifi_setup_requested && companion_wifi_setup_stop_requested);
|
||||
assert(home.handleInput(static_cast<char>(KEY_RIGHT)));
|
||||
assert(home._page == HomeScreen::FIRST);
|
||||
assert(!companion_wifi_setup_requested && companion_wifi_setup_stop_requested);
|
||||
assert(home.handleInput(static_cast<char>(KEY_LEFT)));
|
||||
assert(home._page == HomeScreen::WIFI_SETUP);
|
||||
requestCompanionWiFiSetupStop();
|
||||
struct Backend : mesh::wireless::Backend {
|
||||
uint8_t available() const override { return mesh::wireless::WiFi; }
|
||||
uint8_t enabled() const override { return mesh::wireless::WiFi; }
|
||||
uint8_t clients() const override { return 0; }
|
||||
mesh::wireless::Result set(uint8_t, bool) override { return mesh::wireless::Result::Done; }
|
||||
} backend;
|
||||
mesh::wireless::control().begin(backend);
|
||||
char reply[160] = {};
|
||||
mesh::wireless::control().handle("set wifi off force", reply, sizeof(reply), 0, mesh::wireless::Independent);
|
||||
mesh::wireless::control().service(250);
|
||||
requestCompanionWiFiSetup();
|
||||
assert(!companion_wifi_setup_requested && companion_wifi_setup_stop_requested);
|
||||
}
|
||||
'''
|
||||
source = (source.replace("@REQUESTS@", requests).replace("@PAGES@", page_enum)
|
||||
.replace("@VISIBLE@", visible).replace("@NAVIGATION@", navigation)
|
||||
.replace("@HANDLER@", handler))
|
||||
for char_mode in ("-fsigned-char", "-funsigned-char"):
|
||||
self.compile_and_run(source, char_mode)
|
||||
|
||||
loop = ui[ui.index("void UITask::loop()") : ui.index(
|
||||
"char UITask::checkDisplayOn", ui.index("void UITask::loop()")
|
||||
@@ -124,9 +199,10 @@ class HeltecV4WiFiSetupPageTest(unittest.TestCase):
|
||||
self.assertIn("current boot session", wifi)
|
||||
|
||||
main = MAIN.read_text(encoding="utf-8")
|
||||
requests_start = main.index("void requestCompanionWiFiSetup()")
|
||||
requests_end = main.index("bool toggleCompanionWiFi()", requests_start)
|
||||
requests = main[requests_start:requests_end]
|
||||
requests = "\n".join(extract_braced(main, signature) for signature in (
|
||||
" void requestCompanionWiFiSetup()", " void requestCompanionWiFiSetupStop()"))
|
||||
# Inspect the exact session request methods, not the unrelated permanent
|
||||
# master switch that is now defined between them and toggleCompanionWiFi.
|
||||
self.assertIn("companion_wifi_setup_requested = true;", requests)
|
||||
self.assertIn("companion_wifi_setup_stop_requested = true;", requests)
|
||||
self.assertNotIn("savePrefs", requests)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the production NVS writer/resolver, CLI setters and MQTT consumer."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class FixtureFailure(AssertionError):
|
||||
pass
|
||||
|
||||
|
||||
class MQTTCanonicalWiFiTest(unittest.TestCase):
|
||||
def fixture(self):
|
||||
web = (ROOT / "src/helpers/esp32/WebConfigServer.cpp").read_text()
|
||||
mqtt = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text()
|
||||
fixture = (ROOT / "test/fixtures/mqtt_canonical_wifi.cpp").read_text()
|
||||
web_methods = "\n".join(extract_braced(web, signature) for signature in (
|
||||
"mesh::wifi::CredentialState WebConfigServer::resolveWiFi(",
|
||||
"bool WebConfigServer::hasConfiguredWiFi(",
|
||||
"bool WebConfigServer::loadStandaloneWiFi(",
|
||||
"bool WebConfigServer::saveStandaloneWiFi(",
|
||||
"static bool loadWiFiEdit(",
|
||||
"bool WebConfigServer::setStandaloneWiFiSSID(",
|
||||
"bool WebConfigServer::setStandaloneWiFiPassword(",
|
||||
"bool WebConfigServer::setStandaloneWiFiPowerSave(",
|
||||
"bool WebConfigServer::formatWiFiPassword(",
|
||||
"bool WebConfigServer::reloadStandaloneWiFi(",
|
||||
))
|
||||
batch_start = web.index(' if (_standalone_wifi && strcmp(e.key, "wifi.ssid")')
|
||||
batch_end = web.index(" _cb->execCommand(e.cmd, e.reply);", batch_start)
|
||||
credential_batch = web[batch_start:batch_end]
|
||||
commit_batch = extract_braced(web, " if (_standalone_wifi_dirty)")
|
||||
observer = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text()
|
||||
cli_set = extract_braced(observer[observer.index("bool CommonCLI::handleObserverSetCmd"):],
|
||||
" if (_callbacks->usesCanonicalWiFi())")
|
||||
cli_get = extract_braced(observer[observer.index("bool CommonCLI::handleObserverGetCmd"):],
|
||||
" if (_callbacks->usesCanonicalWiFi())")
|
||||
mqtt_methods = "\n".join(extract_braced(mqtt, signature) for signature in (
|
||||
"bool MQTTBridge::prepareWiFiCredentials(",
|
||||
"void MQTTBridge::beginWiFiStation(",
|
||||
"static bool isWiFiConfigValid(",
|
||||
"static bool customEndpointComplete(",
|
||||
"bool MQTTBridge::isConfigValid(",
|
||||
"bool MQTTBridge::isReady(",
|
||||
))
|
||||
begin = mqtt.index(" // Check if WiFi credentials are configured first")
|
||||
begin_end = mqtt.index(" // These are begin()/end()-scoped", begin)
|
||||
# These are the exact production early gate and the reconnect call;
|
||||
# expensive task/TLS allocation below the gate is a hardware endpoint.
|
||||
gate = mqtt[begin:begin_end]
|
||||
reconnect_start = mqtt.index(" WiFi.disconnect();", mqtt.index("bool MQTTBridge::handleWiFiConnection"))
|
||||
reconnect_start = mqtt.index("#ifdef ESP_PLATFORM", reconnect_start)
|
||||
reconnect_end = mqtt.index("#endif", reconnect_start) + len("#endif")
|
||||
reconnect = mqtt[reconnect_start:reconnect_end]
|
||||
return (fixture.replace("@WEB_METHODS@", web_methods)
|
||||
.replace("@MQTT_METHODS@", mqtt_methods)
|
||||
.replace("@BEGIN_GATE@", gate).replace("@RECONNECT@", reconnect)
|
||||
.replace("@CLI_SET@", cli_set).replace("@CLI_GET@", cli_get)
|
||||
.replace("@CREDENTIAL_BATCH@", credential_batch)
|
||||
.replace("@COMMIT_BATCH@", commit_batch))
|
||||
|
||||
def compile(self, source, *flags):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++")
|
||||
self.assertIsNotNone(compiler)
|
||||
with tempfile.TemporaryDirectory(prefix="mqtt-canonical-wifi-") as directory:
|
||||
path = Path(directory)
|
||||
(path / "Preferences.h").write_text(
|
||||
(ROOT / "test/fixtures/wifi_credentials_preferences.h").read_text(), encoding="ascii")
|
||||
(path / "test.cpp").write_text(source, encoding="ascii")
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
"-DESP_PLATFORM=1", "-DWITH_MQTT_BRIDGE=1", *flags,
|
||||
"-I", str(path), "-I", str(ROOT / "src"), str(path / "test.cpp"),
|
||||
"-o", str(path / "test")], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
result = subprocess.run([str(path / "test")], capture_output=True,
|
||||
text=True, timeout=15)
|
||||
if result.returncode:
|
||||
raise FixtureFailure(result.stdout + result.stderr)
|
||||
|
||||
def test_portal_cli_upgrade_reconnect_boundaries_and_write_failures(self):
|
||||
self.compile(self.fixture())
|
||||
|
||||
def test_companion_compiled_password_fallback_is_preserved(self):
|
||||
self.compile(self.fixture(), '-DWIFI_PWD="compiled-password"')
|
||||
|
||||
def test_pre_fix_mqtt_gate_rejects_fresh_portal_credentials(self):
|
||||
fixture = self.fixture()
|
||||
stale = fixture.replace("const bool wifi_configured = prepareWiFiCredentials();",
|
||||
"const bool wifi_configured = isWiFiConfigValid(_obs, false);")
|
||||
self.assertNotEqual(stale, fixture)
|
||||
with self.assertRaises(FixtureFailure):
|
||||
self.compile(stale)
|
||||
|
||||
def test_reconnect_cannot_revert_to_legacy_credentials(self):
|
||||
fixture = self.fixture()
|
||||
stale = fixture.replace("mesh::wifi::beginStation(_wifi_ssid, _wifi_password);",
|
||||
"mesh::wifi::beginStation(_obs->wifi_ssid, _obs->wifi_password);")
|
||||
self.assertNotEqual(stale, fixture)
|
||||
with self.assertRaises(FixtureFailure):
|
||||
self.compile(stale)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user