Share one bounded WiFi outage and recovery schedule across MQTT, automatic NTP, Companion station ownership and WebConfig. Preserve enabled settings and foreground access while allowing normal sleep after confirmed physical shutdown. Honor failed SDK client stops before handing off WiFi, protect clean teardown and reconfiguration, and retry transient ESP-NOW channel restoration without repeated disconnects. Add production-method, SDK failure and real sleep regressions to CI. Validation: native MQTT/NTP/clock/reconnect suites; production power and wrapper failure tests; browser, room HTTP, OTA and combined bridge regressions; Full Heltec V4 repeater, room and companion builds with capability, flash and linked-memory checks.
123 lines
5.1 KiB
C++
123 lines
5.1 KiB
C++
// Compile the production WebConfig stop/finalize methods with peripheral
|
|
// doubles. Delayed HTTP request destruction must never turn OTA WiFi off.
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
#include <helpers/RoomWebMailbox.h>
|
|
#include <helpers/WiFiNetworkPowerPolicy.h>
|
|
|
|
static uint32_t millis() { return 123; }
|
|
struct WebConfigBatch {
|
|
static uint32_t scheduleAt(uint32_t now, uint32_t delay) { return now + delay; }
|
|
static bool deadlineReached(uint32_t now, uint32_t at) { return int32_t(now - at) >= 0; }
|
|
};
|
|
struct WCLock { explicit WCLock(void*) {} };
|
|
constexpr int WIFI_STA = 1;
|
|
constexpr int WIFI_AP = 2;
|
|
struct WiFiFake {
|
|
bool sta = true, ap = false, connected = true, allow_sta = true, allow_ap_stop = true;
|
|
unsigned stops = 0, ap_stops = 0;
|
|
bool enableSTA(bool on) { if (!allow_sta) return false; sta = on; return true; }
|
|
bool softAPdisconnect(bool) { ++ap_stops; if (allow_ap_stop) ap = false; return false; }
|
|
int getMode() const { return (sta ? WIFI_STA : 0) | (ap ? WIFI_AP : 0); }
|
|
void mode(int value) { assert(value == WIFI_STA); sta = true; ap = false; }
|
|
} WiFi;
|
|
void stopOwnedWiFiRadio() {
|
|
WiFi.sta = WiFi.ap = WiFi.connected = false;
|
|
++WiFi.stops;
|
|
}
|
|
struct Server { bool listening = true; void end() { listening = false; } };
|
|
struct DNS { void stop() {} };
|
|
struct Callbacks { unsigned stops = 0; void onWebConfigStopped() { ++stops; } };
|
|
struct WebConfigServer {
|
|
enum Mode { MODE_OFF, MODE_LAN, MODE_SETUP };
|
|
enum { STOP_WARN_MS = 1000, BATCH_IDLE = 0, BATCH_PENDING = 1, BATCH_CONFIG = 0 };
|
|
Server server;
|
|
Server* _server = &server;
|
|
DNS* _dns = nullptr;
|
|
Callbacks callbacks;
|
|
Callbacks* _cb = &callbacks;
|
|
mesh::RoomWebMailbox* _room_mailbox = new mesh::RoomWebMailbox;
|
|
Mode _mode = MODE_LAN;
|
|
bool _stopping = false, _keep_wifi_on_stop = false, _was_setup_ap = false;
|
|
bool _owns_wifi = true, _stop_warned = false, _initial_setup = false;
|
|
bool _retry_saved_wifi_in_setup = false, _setup_reconnect_in_progress = false;
|
|
bool _batch_reboot_armed = false, _setup_wifi_handoff_pending = false;
|
|
bool _admin_pwd_set = false, _board_cmds_probed = false;
|
|
uint32_t _stop_warn_at = 0, _setup_started_at = 0, _connect_deadline = 0;
|
|
uint32_t _setup_reconnect_deadline = 0, _reboot_at = 0, _batch_state = 0;
|
|
uint32_t _batch_next = 0, _setup_wifi_handoff_deadline = 0, _batch_kind = 0;
|
|
uint32_t _board_cmds = 0;
|
|
uint32_t _network_activity_until = 0;
|
|
void* _mux = nullptr;
|
|
struct Terminal { bool running = false; struct { bool pending = false; } queue; };
|
|
Terminal* _terminal = nullptr;
|
|
char _wifi_ssid[32] = {}, _setup_wifi_handoff_ip[16] = {};
|
|
char _session_token[16] = {}, _stats_json[16] = {};
|
|
bool routes = true;
|
|
void detachRoutes() { routes = false; }
|
|
void closeTerminal() {}
|
|
uint32_t handlerRefCount() const { return 0; }
|
|
bool isNetworkForegroundActive(uint32_t) const;
|
|
void refreshNetworkPowerHold(uint32_t);
|
|
bool isRunning() const { return _mode != MODE_OFF; }
|
|
bool isStopping() const { return _stopping; }
|
|
void requestStop();
|
|
bool stopForOTA(char*);
|
|
void finalizeTeardown();
|
|
~WebConfigServer() { delete _room_mailbox; }
|
|
};
|
|
@METHODS@
|
|
|
|
int main() {
|
|
for (bool setup : {false, true}) {
|
|
for (bool owns_wifi : {false, true}) {
|
|
WebConfigServer portal;
|
|
portal._owns_wifi = owns_wifi;
|
|
portal._was_setup_ap = setup;
|
|
portal._mode = setup ? WebConfigServer::MODE_SETUP : WebConfigServer::MODE_LAN;
|
|
WiFi = WiFiFake{};
|
|
WiFi.ap = setup;
|
|
WiFi.sta = !setup;
|
|
WiFi.connected = !setup;
|
|
char reply[160] = {};
|
|
assert(portal.stopForOTA(reply));
|
|
assert(strcmp(reply, "WebConfig stopped") == 0);
|
|
assert(!portal.routes && !portal.server.listening && portal.isStopping());
|
|
assert(mesh::wifi::networkPower().foregroundActive());
|
|
assert(WiFi.sta && !WiFi.ap && WiFi.stops == 0);
|
|
assert(WiFi.connected == !setup);
|
|
// OTA can now bind port 80 and create its own AP, while HTTP request
|
|
// objects on the old server still have outstanding references.
|
|
WiFi.ap = true;
|
|
portal.requestStop(); // A duplicate stop cannot revoke the handoff.
|
|
portal.finalizeTeardown(); // Existing requests have finally drained.
|
|
assert(portal._room_mailbox == nullptr);
|
|
assert(WiFi.ap && WiFi.sta && WiFi.stops == 0);
|
|
assert(WiFi.ap_stops == unsigned(setup));
|
|
assert(portal.callbacks.stops == 1 && !portal._keep_wifi_on_stop);
|
|
assert(!mesh::wifi::networkPower().foregroundActive());
|
|
}
|
|
}
|
|
WebConfigServer ordinary;
|
|
WiFi = WiFiFake{};
|
|
ordinary.requestStop();
|
|
ordinary.finalizeTeardown();
|
|
assert(ordinary._room_mailbox == nullptr);
|
|
assert(WiFi.stops == 1 && !WiFi.sta); // Normal stop keeps its old behavior.
|
|
WebConfigServer failed;
|
|
failed._was_setup_ap = true;
|
|
WiFi = WiFiFake{};
|
|
WiFi.allow_sta = false;
|
|
char reply[160] = {};
|
|
assert(!failed.stopForOTA(reply));
|
|
assert(strstr(reply, "ERR:") && failed.server.listening && !failed.isStopping());
|
|
WiFi.allow_sta = true;
|
|
WiFi.ap = true;
|
|
WiFi.allow_ap_stop = false;
|
|
assert(!failed.stopForOTA(reply));
|
|
assert(strstr(reply, "WebConfig stopped; WiFi AP handoff failed"));
|
|
assert(!failed.server.listening);
|
|
}
|