Fix MQTT startup after ESP32 WebConfig provisioning

This commit is contained in:
mikecarper committed 2026-10-06 19:28:10 -07:00
1 parent 745aeb1758
commit 1e421ff4fc
18 files changed
+1018 -208

No files matched your search

+1
View File
@@ -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;
+24 -1
View File
@@ -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) {
+58 -7
View File
@@ -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 {
+12 -1
View File
@@ -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();
+117 -147
View File
@@ -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--) {
+15 -8
View File
@@ -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
+134
View File
@@ -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