Merge fix/mqtt-review-2026-09-09 into feat/abstract-networking

#54 lands first, so the network abstraction is reworked on top of the
per-slot client ownership model instead of alongside it.

Two resolutions are semantic, not textual:

- Link-transition teardown goes through the ownership API. It called
  softDisconnect() on every started client, which bypassed client_state and
  ignored the typed result. A slot that is still Starting now gets a real
  stop: softDisconnect() returns immediately on a client that is not yet
  connected, so its attempt would otherwise complete against the old route and
  deliver a CONNECTED event indistinguishable from the new one's (F04). On a
  medium switch the old route can still be briefly usable, so that is not
  hypothetical. A connected client keeps the cheap bounded path, and a
  quarantined one is left alone.

- NetworkLink::applyPowerPrefs() adopts WifiPowerSavePolicy. Its local
  `2 ? MAX : NONE` mapping would have reintroduced F11 and read the new stored
  value 3 (explicit `min`) as none. It also applies the setting when the link
  starts already associated, which is the case the bridge used to cover.

The NTP probe from #54 keeps its validation and its one-attempt-per-server
bound, but resolves and gates on the selected link rather than on WiFi, so it
works on an Ethernet-preferred node.
This commit is contained in:
agessaman committed 2026-09-19 15:15:07 -07:00
commit 7f28231b25
30 files changed
+3075 -451

No files matched your search

+8 -22
View File
@@ -16,6 +16,7 @@
#include "AlertReporter.h" // for alertReporterBannedChannelMatch[Hex]()
#include "MQTTObserverValidation.h" // pure input validators (host-testable)
#include "NetworkLink.h"
#include "WifiPowerSavePolicy.h" // one powersave value->mode/name mapping
#include <Utils.h>
#include <climits>
#include <new>
@@ -441,42 +442,29 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (!persistObserverPrefs(reply)) return true;
strcpy(reply, "OK");
} else if (memcmp(config, "wifi.powersave ", 15) == 0) {
const char* value = &config[15];
uint8_t ps_value;
bool valid = false;
if (memcmp(value, "min", 3) == 0 && (value[3] == 0 || value[3] == ' ')) {
ps_value = 0;
valid = true;
} else if (memcmp(value, "none", 4) == 0 && (value[4] == 0 || value[4] == ' ')) {
ps_value = 1;
valid = true;
} else if (memcmp(value, "max", 3) == 0 && (value[3] == 0 || value[3] == ' ')) {
ps_value = 2;
valid = true;
}
if (!valid) {
if (!WifiPowerSavePolicy::parseName(&config[15], &ps_value)) {
strcpy(reply, "Error: must be none, min, or max");
} else {
_mqtt_prefs.wifi_power_save = ps_value;
if (!persistObserverPrefs(reply)) return true;
const char* ps_name = WifiPowerSavePolicy::nameFor(ps_value);
#ifdef ESP_PLATFORM
if (strcmp(activeNetworkLink().mediumName(), "wifi") == 0 &&
activeNetworkLink().isConnected()) {
wifi_ps_type_t ps_mode = (ps_value == 1) ? WIFI_PS_NONE :
(ps_value == 2) ? WIFI_PS_MAX_MODEM : WIFI_PS_MIN_MODEM;
esp_err_t ps_result = esp_wifi_set_ps(ps_mode);
// Same mapping the link applies on every association, so this cannot
// drift back apart (see WifiPowerSavePolicy).
esp_err_t ps_result =
esp_wifi_set_ps((wifi_ps_type_t)WifiPowerSavePolicy::modeFor(ps_value));
if (ps_result == ESP_OK) {
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
sprintf(reply, "OK - power save set to %s", ps_name);
} else {
sprintf(reply, "OK - saved, but failed to apply: %d", ps_result);
}
} else {
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
sprintf(reply, "OK - saved as %s (will apply on next WiFi connection)", ps_name);
}
#else
const char* ps_name = (ps_value == 1) ? "none" : (ps_value == 2) ? "max" : "min";
sprintf(reply, "OK - saved as %s", ps_name);
#endif
}
@@ -1121,9 +1109,7 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
#endif
}
} else if (memcmp(config, "wifi.powersave", 14) == 0) {
uint8_t ps = _mqtt_prefs.wifi_power_save;
const char* ps_name = (ps == 1) ? "none" : (ps == 2) ? "max" : "min";
sprintf(reply, "> %s", ps_name);
sprintf(reply, "> %s", WifiPowerSavePolicy::nameFor(_mqtt_prefs.wifi_power_save));
} else if (memcmp(config, "timezone.offset", 15) == 0) {
// Must precede the "timezone" (8-byte) check below — that prefix-matches
// "timezone.offset" too, so the more-specific key has to come first or