// Only peripheral endpoints are doubles. The readiness helper and startup // method are extracted verbatim from production WebConfigServer.cpp. #include #include #include #include #include #include #include #include using esp_err_t = int; using wifi_mode_t = int; using wifi_auth_mode_t = int; constexpr esp_err_t ESP_OK = 0; constexpr int WIFI_AUTH_OPEN = 0, WIFI_AUTH_WPA2_PSK = 3; constexpr int WL_CONNECTED = 3, WL_DISCONNECTED = 6; constexpr int WIFI_SCAN_FAILED = -2, WIFI_SCAN_RUNNING = -1; constexpr int WIFI_OFF = 0, WIFI_STA = 1, WIFI_AP = 2, WIFI_AP_STA = 3; constexpr int WIFI_MODE_NULL = WIFI_OFF, WIFI_MODE_STA = WIFI_STA; constexpr int WIFI_MODE_AP = WIFI_AP, WIFI_MODE_APSTA = WIFI_AP_STA; constexpr int WIFI_IF_STA = 0, WIFI_IF_AP = 1; constexpr uint8_t WIFI_PROTOCOL_11B = 1, WIFI_PROTOCOL_11G = 2; constexpr uint8_t WIFI_PROTOCOL_11N = 4, WIFI_PROTOCOL_LR = 8; constexpr int WIFI_AP_STARTED_BIT = 1 << 0; constexpr int AP_STARTED_BIT = WIFI_AP_STARTED_BIT; constexpr int MALLOC_CAP_INTERNAL = 1; #define WEBCONFIG_AP_PREFIX "MC" #ifndef ESP_ARDUINO_VERSION_MAJOR #define ESP_ARDUINO_VERSION_MAJOR 2 #endif struct IPAddress { uint32_t value = 0; IPAddress() = default; IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d) : value((uint32_t(a) << 24) | (uint32_t(b) << 16) | (uint32_t(c) << 8) | d) {} explicit operator uint32_t() const { return value; } std::string toString() const { char text[16]; snprintf(text, sizeof(text), "%u.%u.%u.%u", (value >> 24) & 255, (value >> 16) & 255, (value >> 8) & 255, value & 255); return text; } }; struct wifi_config_t { struct { uint8_t ssid[32] = {}; uint8_t ssid_len = 0, ssid_hidden = 0, channel = 1; wifi_auth_mode_t authmode = WIFI_AUTH_OPEN; } ap; }; enum class Fault { None, StaleSsid, HiddenSsid, WrongChannel, WrongProtocol, MissingMode, NotStarted, ZeroIp, DelayedStart, StaleOnce, SetterFailure, WrongAuth, StillConnected, ModeSetterFailure, IpConfigSetterFailure, StaProtocolSetterFailure, SdkModeReadFailure, SdkConfigReadFailure, SdkProtocolReadFailure }; enum class ScanBehavior { Complete, SlowComplete, Failure, Stuck, StopDelayed, Unstoppable, FacadeFailedActive, FacadeFailedStopRejected }; constexpr wifi_auth_mode_t expectedAuth() { #ifdef WEBCONFIG_AP_PASSWORD return WEBCONFIG_AP_PASSWORD[0] ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN; #else return WIFI_AUTH_OPEN; #endif } void delay(unsigned amount); struct WiFiFake { bool initialized = true, shared_espnow = false, auto_reconnect = true; bool configured_ip = false, ap_started = false; bool delayed_stop = false; unsigned pending_stop_ms = 0; unsigned stop_delay_ms = 160, config_delay_ms = 0; struct APStatus { bool started() const; } AP; int live_mode = WIFI_STA; uint8_t live_protocol = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N; wifi_config_t live_config; IPAddress ap_ip{192, 168, 4, 1}, planned_ip{192, 168, 4, 1}; Fault fault = Fault::None; ScanBehavior scan_behavior = ScanBehavior::Complete; unsigned ap_calls = 0, config_calls = 0, sta_only_calls = 0; unsigned ap_stops = 0, driver_stops = 0, driver_deinits = 0; unsigned polls = 0, scans = 0; unsigned scan_deletes = 0, scan_queries = 0; unsigned sdk_scan_stops = 0, scan_remaining_ms = 0; unsigned stale_picker_results = 2; int scan_result = 2; bool departed_home_channel = false, scan_stop_pending = false, sdk_scan_active = false; unsigned scans_during_ap = 0; bool mode(int mode) { assert(initialized); if (fault == Fault::ModeSetterFailure) return false; if (mode == WIFI_STA) ++sta_only_calls; const bool removing_ap = (live_mode & WIFI_AP) && !(mode & WIFI_AP); live_mode = mode; if (!(mode & WIFI_AP)) { if (delayed_stop && ap_started) { if (removing_ap) pending_stop_ms = stop_delay_ms; } else ap_started = false; } return true; } int getMode() const { return live_mode; } int status() const { return fault == Fault::StillConnected ? WL_CONNECTED : WL_DISCONNECTED; } bool setAutoReconnect(bool enabled) { auto_reconnect = enabled; return true; } bool disconnect(bool wifioff, bool erase) { assert(!wifioff && erase); return true; } bool softAPConfig(IPAddress ip, IPAddress gateway, IPAddress mask) { ++config_calls; assert(ip.value == IPAddress(192, 168, 4, 1).value && gateway.value == ip.value); assert(mask.value == IPAddress(255, 255, 255, 0).value); if (fault == Fault::IpConfigSetterFailure) return false; assert(scan_result != WIFI_SCAN_RUNNING && !sdk_scan_active); if (config_delay_ms) delay(config_delay_ms); configured_ip = true; planned_ip = ip; live_mode |= WIFI_AP; // Arduino's configuration call enables the AP. return true; } bool softAP(const char* ssid, const char* password, int channel) { #ifdef WEBCONFIG_AP_PASSWORD assert(password != nullptr && strcmp(password, WEBCONFIG_AP_PASSWORD) == 0); #else assert(password == nullptr); #endif assert(channel == 6); ++ap_calls; polls = 0; if (fault == Fault::SetterFailure) return false; assert(pending_stop_ms == 0); // A new start must not inherit old AP_START. live_mode |= WIFI_AP; live_config = {}; const bool stale = fault == Fault::StaleSsid || (fault == Fault::StaleOnce && ap_calls == 1); const char* actual = stale ? "MeshCore-OTA" : ssid; live_config.ap.ssid_len = uint8_t(strlen(actual)); memcpy(live_config.ap.ssid, actual, strlen(actual)); live_config.ap.ssid_hidden = fault == Fault::HiddenSsid ? 1 : 0; live_config.ap.channel = fault == Fault::WrongChannel ? 11 : uint8_t(channel); live_config.ap.authmode = fault == Fault::WrongAuth ? (expectedAuth() == WIFI_AUTH_OPEN ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN) : expectedAuth(); ap_ip = fault == Fault::ZeroIp ? IPAddress() : planned_ip; ap_started = fault != Fault::NotStarted && fault != Fault::DelayedStart; // Every injected state error deliberately returns success here. return true; } bool softAPdisconnect(bool off) { assert(off); ++ap_stops; live_mode &= ~WIFI_AP; if (delayed_stop && ap_started) pending_stop_ms = stop_delay_ms; else ap_started = false; return true; } IPAddress softAPIP() const { return ap_ip; } int getStatusBits() const { return ap_started ? WIFI_AP_STARTED_BIT : 0; } int scanNetworks(bool async, bool hidden, bool passive, unsigned maximum, uint8_t channel) { assert(async && !hidden && !passive && (maximum == 200 || maximum == 300)); assert(channel == (shared_espnow ? 6 : 0)); if (live_mode & WIFI_AP) ++scans_during_ap; if (maximum == 200) assert(live_mode == WIFI_STA && !ap_started); ++scans; scan_result = WIFI_SCAN_RUNNING; sdk_scan_active = true; if (channel == 0) departed_home_channel = true; if (scan_behavior == ScanBehavior::Failure) { scan_result = WIFI_SCAN_FAILED; sdk_scan_active = false; return WIFI_SCAN_FAILED; } if (scan_behavior == ScanBehavior::Complete) scan_remaining_ms = 100; else if (scan_behavior == ScanBehavior::SlowComplete) scan_remaining_ms = 3500; else if (scan_behavior == ScanBehavior::FacadeFailedActive || scan_behavior == ScanBehavior::FacadeFailedStopRejected) scan_result = WIFI_SCAN_FAILED; // Facade timeout hides the live SDK scanner. return WIFI_SCAN_RUNNING; } void scanDelete() { ++scan_deletes; stale_picker_results = 0; // Arduino deletes result storage; it does not cancel an SDK scan. if (!sdk_scan_active) scan_result = WIFI_SCAN_FAILED; } int scanComplete() { ++scan_queries; return scan_result; } const char* SSID(int) const { return "cached-network"; } int RSSI(int) const { return -50; } int encryptionType(int) const { return WIFI_AUTH_OPEN; } int channel(int) const { return 6; } } WiFi; bool WiFiFake::APStatus::started() const { return WiFi.ap_started; } static uint32_t clock_ms = 100; uint32_t millis() { return clock_ms; } void delay(unsigned amount) { clock_ms += amount; if (WiFi.pending_stop_ms) { WiFi.pending_stop_ms -= std::min(amount, WiFi.pending_stop_ms); if (!WiFi.pending_stop_ms) WiFi.ap_started = false; } if (amount == 20 && ++WiFi.polls >= 3 && WiFi.fault == Fault::DelayedStart && (WiFi.live_mode & WIFI_AP)) WiFi.ap_started = true; if (WiFi.scan_remaining_ms) { WiFi.scan_remaining_ms -= std::min(amount, WiFi.scan_remaining_ms); if (!WiFi.scan_remaining_ms) { WiFi.scan_result = WiFi.scan_stop_pending ? WIFI_SCAN_FAILED : 2; WiFi.sdk_scan_active = false; WiFi.scan_stop_pending = false; } } } int esp_wifi_scan_stop() { ++WiFi.sdk_scan_stops; if (WiFi.scan_behavior == ScanBehavior::Unstoppable || WiFi.scan_behavior == ScanBehavior::FacadeFailedStopRejected) return -1; if (WiFi.scan_behavior == ScanBehavior::StopDelayed) { WiFi.scan_stop_pending = true; WiFi.scan_remaining_ms = 60; } else { WiFi.scan_result = WIFI_SCAN_FAILED; WiFi.sdk_scan_active = false; } return ESP_OK; } esp_err_t esp_wifi_get_mode(wifi_mode_t* mode) { if (WiFi.fault == Fault::SdkModeReadFailure) return -1; *mode = WiFi.fault == Fault::MissingMode ? WIFI_STA : WiFi.live_mode; return ESP_OK; } esp_err_t esp_wifi_get_config(int interface, wifi_config_t* config) { assert(interface == WIFI_IF_AP); if (WiFi.fault == Fault::SdkConfigReadFailure) return -1; *config = WiFi.live_config; return ESP_OK; } esp_err_t esp_wifi_get_protocol(int interface, uint8_t* protocol) { assert(interface == WIFI_IF_AP); if (WiFi.fault == Fault::SdkProtocolReadFailure) return -1; *protocol = WiFi.fault == Fault::WrongProtocol ? WIFI_PROTOCOL_LR : WiFi.live_protocol; return ESP_OK; } esp_err_t esp_wifi_set_protocol(int interface, uint8_t protocol) { if (interface == WIFI_IF_AP) WiFi.live_protocol = protocol; else { assert(interface == WIFI_IF_STA); if (WiFi.fault == Fault::StaProtocolSetterFailure) return -1; } return ESP_OK; } int esp_wifi_stop() { ++WiFi.driver_stops; return ESP_OK; } int esp_wifi_deinit() { ++WiFi.driver_deinits; WiFi.initialized = false; return ESP_OK; } unsigned heap_caps_get_largest_free_block(int) { return 100000; } struct { unsigned getFreeHeap() const { return 100000; } } ESP; namespace mesh { struct DebugPort { template void printf(const char*, Args...) {} }; DebugPort& usbDebugPort() { static DebugPort port; return port; } namespace wireless { constexpr uint8_t WiFi = 1; struct Control { bool disabled = false; bool blocked(uint8_t) const { return disabled; } }; Control& control() { static Control control; return control; } } namespace wifi { @AP_MASK@ constexpr uint8_t kProtocolMask = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR; int accessPointChannel() { return 6; } uint8_t stationScanChannel() { return WiFi.shared_espnow ? 6 : 0; } esp_err_t applyAccessPointProtocolMask() { return esp_wifi_set_protocol(WIFI_IF_AP, kAccessPointProtocolMask); } esp_err_t applyProtocolMask(int interface) { return esp_wifi_set_protocol(interface, kProtocolMask); } } } void stopOwnedWiFiRadio() { if (WiFi.shared_espnow) WiFi.mode(WIFI_STA); else WiFi.live_mode = WIFI_OFF; } struct DNSServer { static unsigned starts; void start(int port, const char* wildcard, IPAddress ip) { assert(port == 53 && strcmp(wildcard, "*") == 0 && ip.value != 0); ++starts; } }; unsigned DNSServer::starts = 0; struct NodeSnapshot { char admin_password[16] = {}; }; struct Callbacks { void getNodeSnapshot(NodeSnapshot& node) { strcpy(node.admin_password, "configured"); } }; struct AsyncResponseStream { std::string state; unsigned networks = 0; }; struct AsyncWebServerRequest { int status = 0; bool rescan = false; std::string payload; unsigned completed_networks = 0; AsyncResponseStream response; bool hasParam(const char* name) const { return rescan && strcmp(name, "rescan") == 0; } void send(int value) { status = value; } void send(int value, const char* type, const char* body) { assert(strcmp(type, "application/json") == 0); status = value; payload = body; } AsyncResponseStream* beginResponseStream(const char*) { return &response; } void send(AsyncResponseStream* value) { assert(value == &response); status = 200; payload = "{\"state\":\"" + value->state + "\"}"; completed_networks = value->networks; } }; // Serialization is an endpoint double; tests below exercise the complete // production scan-request control flow rather than a copied branch. struct JsonValue { std::string* text = nullptr; void operator=(const char* value) { if (text) *text = value; } template void operator=(const T&) {} }; struct JsonObject { JsonValue operator[](const char*) { return {}; } }; struct JsonArray { unsigned* count; JsonObject createNestedObject() { ++*count; return {}; } }; struct DynamicJsonDocument { std::string state; unsigned networks = 0; explicit DynamicJsonDocument(unsigned) {} JsonValue operator[](const char*) { return {&state}; } JsonArray createNestedArray(const char*) { return {&networks}; } }; void serializeJson(DynamicJsonDocument& doc, AsyncResponseStream& response) { response.state = doc.state; response.networks = doc.networks; } struct WebConfigServer { enum Mode { MODE_OFF, MODE_LAN, MODE_SETUP }; Mode _mode = MODE_OFF; bool _owns_wifi = true, _stopping = false; bool _retry_saved_wifi_in_setup = true, _setup_reconnect_in_progress = true; bool _was_setup_ap = false, _initial_setup = false; uint32_t _setup_reconnect_deadline = 5, _setup_started_at = 5, _last_activity = 0; const uint8_t _pub_key[2] = {0x62, 0xF2}; char _ap_ssid[33] = {}, _wifi_ssid[32] = {}; DNSServer* _dns = nullptr; Callbacks callbacks; Callbacks* _cb = &callbacks; unsigned servers = 0; bool auth_allowed = true; ~WebConfigServer() { delete _dns; } void createServer() { ++servers; } void requestNetworkAccess() {} void refreshNetworkPowerHold(uint32_t) {} bool startSetupMode(char reply[]); bool checkAuth(AsyncWebServerRequest*) const { return auth_allowed; } void handleScan(AsyncWebServerRequest* req); }; @METHODS@ void requireEndpoint(const WebConfigServer& portal, const char* reply, bool cached_scan = true) { assert(portal._mode == WebConfigServer::MODE_SETUP && portal._dns != nullptr); assert(WiFi.live_mode == WIFI_AP_STA && WiFi.ap_started); assert(WiFi.live_config.ap.ssid_len == 7); assert(memcmp(WiFi.live_config.ap.ssid, "MC-62F2", 7) == 0); assert(!WiFi.live_config.ap.ssid_hidden && WiFi.live_config.ap.channel == 6); assert(WiFi.live_config.ap.authmode == expectedAuth()); assert(WiFi.live_protocol == mesh::wifi::kAccessPointProtocolMask); assert(WiFi.ap_ip.value != 0 && WiFi.config_calls > 0 && WiFi.sta_only_calls > 0); assert(WiFi.scans == 1 && WiFi.scans_during_ap == 0 && DNSServer::starts == 1); const bool stopped_scan = WiFi.scan_behavior != ScanBehavior::Complete && WiFi.scan_behavior != ScanBehavior::SlowComplete; assert(WiFi.scan_deletes == (stopped_scan ? 2u : 1u) && WiFi.stale_picker_results == 0); assert(WiFi.scan_result == (cached_scan ? 2 : WIFI_SCAN_FAILED)); assert(!WiFi.sdk_scan_active && WiFi.sdk_scan_stops == (stopped_scan ? 1u : 0u)); assert(!WiFi.auto_reconnect && !portal._retry_saved_wifi_in_setup); assert(strstr(reply, "join 'MC-62F2' then open http://192.168.4.1/")); assert(portal.servers == 1); assert(WiFi.initialized && WiFi.driver_stops == 0 && WiFi.driver_deinits == 0); } void reset(Fault fault = Fault::None) { WiFi = WiFiFake{}; WiFi.fault = fault; clock_ms = 100; DNSServer::starts = 0; mesh::wireless::control().disabled = false; } int main() { // A live LR transport must remain untouched instead of being silently // downgraded when a conventional setup access point is requested. for (uint8_t owner : {mesh::wifi::kLongRangeRadioOwner, mesh::wifi::kLongRangeBridgeOwner}) { reset(); mesh::wifi::setLongRangeOwner(owner, true); WebConfigServer portal; char reply[160] = {}; assert(!portal.startSetupMode(reply)); assert(strstr(reply, "stop ESP-NOW") && WiFi.live_mode == WIFI_STA); assert(WiFi.sta_only_calls == 0 && WiFi.ap_calls == 0 && WiFi.scans == 0); mesh::wifi::setLongRangeOwner(owner, false); } for (Fault fault : {Fault::StaleSsid, Fault::HiddenSsid, Fault::WrongChannel, Fault::WrongProtocol, Fault::MissingMode, Fault::NotStarted, Fault::ZeroIp, Fault::SetterFailure, Fault::WrongAuth, Fault::StillConnected, Fault::ModeSetterFailure, Fault::IpConfigSetterFailure, Fault::StaProtocolSetterFailure, Fault::SdkModeReadFailure, Fault::SdkConfigReadFailure, Fault::SdkProtocolReadFailure}) { reset(fault); WiFi.shared_espnow = true; WebConfigServer portal; char reply[160] = {}; const uint32_t started = millis(); assert(!portal.startSetupMode(reply)); assert(millis() - started < 4500); assert(portal._mode == WebConfigServer::MODE_OFF && !portal._dns && portal.servers == 0); const unsigned prepared = fault != Fault::ModeSetterFailure && fault != Fault::StaProtocolSetterFailure ? 1u : 0u; assert(!strstr(reply, "AP started") && WiFi.scans == prepared && WiFi.scan_deletes == prepared && WiFi.scans_during_ap == 0 && DNSServer::starts == 0); assert(WiFi.ap_calls <= 6 && WiFi.initialized && (WiFi.live_mode & WIFI_STA)); assert(WiFi.driver_stops == 0 && WiFi.driver_deinits == 0); } // Slow asynchronous stops and successful-but-stale setters must remain a // bounded CLI request, rather than exhausting every per-attempt timeout. reset(Fault::StaleSsid); WiFi.shared_espnow = true; WiFi.delayed_stop = true; WiFi.stop_delay_ms = 280; WiFi.config_delay_ms = 100; WiFi.live_mode = WIFI_AP_STA; WiFi.ap_started = true; WebConfigServer slow_failure; char slow_reply[160] = {}; const uint32_t slow_started = millis(); assert(!slow_failure.startSetupMode(slow_reply)); assert(millis() - slow_started < 4500); assert(WiFi.ap_calls < 6 && !slow_failure._dns && slow_failure.servers == 0); assert(WiFi.initialized && (WiFi.live_mode & WIFI_STA)); assert(!strstr(slow_reply, "AP started")); for (bool shared_espnow : {false, true}) { for (Fault fault : {Fault::None, Fault::DelayedStart, Fault::StaleOnce}) { reset(fault); WiFi.shared_espnow = shared_espnow; WebConfigServer portal; char reply[160] = {}; assert(portal.startSetupMode(reply)); requireEndpoint(portal, reply); if (fault == Fault::StaleOnce) assert(WiFi.ap_calls >= 2); if (fault == Fault::DelayedStart) assert(WiFi.polls >= 3); } } reset(); WiFi.scan_behavior = ScanBehavior::SlowComplete; WebConfigServer slow_success; char slow_success_reply[160] = {}; const uint32_t long_scan_started = millis(); assert(slow_success.startSetupMode(slow_success_reply)); assert(millis() - long_scan_started < 4500); requireEndpoint(slow_success, slow_success_reply); // Completing just before the pre-scan deadline must still leave enough // time to bring up a delayed AP after an asynchronous old AP shutdown. reset(Fault::DelayedStart); WiFi.scan_behavior = ScanBehavior::SlowComplete; WiFi.delayed_stop = true; WiFi.stop_delay_ms = 280; WiFi.config_delay_ms = 100; WiFi.live_mode = WIFI_AP_STA; WiFi.ap_started = true; WebConfigServer slow_handoff; char slow_handoff_reply[160] = {}; const uint32_t slow_handoff_started = millis(); assert(slow_handoff.startSetupMode(slow_handoff_reply)); assert(millis() - slow_handoff_started < 4500); requireEndpoint(slow_handoff, slow_handoff_reply); // A failed first AP attempt after a long successful scan must report // failure truthfully and return within the console reply deadline. reset(Fault::StaleSsid); WiFi.scan_behavior = ScanBehavior::SlowComplete; WiFi.shared_espnow = true; WiFi.delayed_stop = true; WiFi.stop_delay_ms = 280; WiFi.config_delay_ms = 100; WiFi.live_mode = WIFI_AP_STA; WiFi.ap_started = true; WebConfigServer slow_scan_failure; char slow_scan_failure_reply[160] = {}; const uint32_t slow_scan_failure_started = millis(); assert(!slow_scan_failure.startSetupMode(slow_scan_failure_reply)); assert(millis() - slow_scan_failure_started < 4500); assert(!slow_scan_failure._dns && slow_scan_failure.servers == 0 && WiFi.scans == 1); assert(!strstr(slow_scan_failure_reply, "AP started")); // Maximum scan time, delayed SDK cancellation, old AP shutdown, and // unsuccessful AP readiness may exceed 4.5 seconds together. They must // still fit the actual five-second console reply deadline. reset(Fault::StaleSsid); WiFi.scan_behavior = ScanBehavior::StopDelayed; WiFi.shared_espnow = true; WiFi.delayed_stop = true; WiFi.stop_delay_ms = 280; WiFi.config_delay_ms = 100; WiFi.live_mode = WIFI_AP_STA; WiFi.ap_started = true; WebConfigServer exhausted_scan; char exhausted_scan_reply[160] = {}; const uint32_t exhausted_scan_started = millis(); assert(!exhausted_scan.startSetupMode(exhausted_scan_reply)); assert(millis() - exhausted_scan_started < 5000); assert(!exhausted_scan._dns && exhausted_scan.servers == 0 && WiFi.scans == 1); assert(WiFi.sdk_scan_stops == 1 && !WiFi.sdk_scan_active); assert(!strstr(exhausted_scan_reply, "AP started")); for (ScanBehavior scan_behavior : {ScanBehavior::Failure, ScanBehavior::Stuck, ScanBehavior::StopDelayed, ScanBehavior::FacadeFailedActive}) { reset(); WiFi.scan_behavior = scan_behavior; WebConfigServer portal; char reply[160] = {}; const uint32_t started = millis(); assert(portal.startSetupMode(reply)); assert(millis() - started < 4500); requireEndpoint(portal, reply, false); // An ordinary picker request must preserve the stable AP after a failed // pre-scan. A user-requested rescan can retry without an automatic loop. const unsigned scan_stops = WiFi.sdk_scan_stops; const unsigned scan_deletes = WiFi.scan_deletes; AsyncWebServerRequest scan; for (unsigned request = 0; request < 2; ++request) { portal.handleScan(&scan); assert(scan.status == 200); assert(scan.payload.find("\"state\":\"done\"") != std::string::npos); assert(scan.payload.find("\"networks\":[]") != std::string::npos); assert(scan.payload.find("\"scan_failed\":true") != std::string::npos); assert(WiFi.scans == 1 && WiFi.scans_during_ap == 0); assert(WiFi.sdk_scan_stops == scan_stops && WiFi.scan_deletes == scan_deletes); assert(!WiFi.sdk_scan_active && WiFi.scan_result == WIFI_SCAN_FAILED); } WiFi.scan_behavior = ScanBehavior::Complete; scan.rescan = true; portal.handleScan(&scan); assert(scan.status == 200 && scan.payload == "{\"state\":\"scanning\"}"); assert(WiFi.scans == 2 && WiFi.scans_during_ap == 1 && WiFi.sdk_scan_active); portal.handleScan(&scan); assert(WiFi.scans == 2); delay(100); scan.rescan = false; portal.handleScan(&scan); assert(scan.payload == "{\"state\":\"done\"}" && scan.completed_networks == 2); assert(WiFi.scans == 2 && WiFi.scan_deletes == scan_deletes); assert(WiFi.sdk_scan_stops == scan_stops && !WiFi.sdk_scan_active); } for (ScanBehavior scan_behavior : {ScanBehavior::Unstoppable, ScanBehavior::FacadeFailedStopRejected}) { reset(); WiFi.scan_behavior = scan_behavior; WiFi.shared_espnow = true; WebConfigServer stuck_scanner; char stuck_reply[160] = {}; const uint32_t stuck_started = millis(); assert(!stuck_scanner.startSetupMode(stuck_reply)); assert(millis() - stuck_started < 4500 && WiFi.scans == 1 && WiFi.sdk_scan_stops == 1); assert(WiFi.sdk_scan_active && WiFi.ap_calls == 0 && WiFi.config_calls == 0); assert(!stuck_scanner._dns && stuck_scanner.servers == 0 && !strstr(stuck_reply, "AP started")); } // Reuse after an OTA AP: the existing SSID/protocol must be replaced, while // the shared STA driver remains alive. Repeat after the setup AP is stopped. reset(); WiFi.shared_espnow = true; WiFi.delayed_stop = true; WiFi.live_mode = WIFI_AP_STA; WiFi.ap_started = true; memcpy(WiFi.live_config.ap.ssid, "MeshCore-OTA", 12); WiFi.live_config.ap.ssid_len = 12; WiFi.live_protocol = WIFI_PROTOCOL_LR; for (int iteration = 0; iteration < 2; ++iteration) { WebConfigServer portal; char reply[160] = {}; assert(portal.startSetupMode(reply)); requireEndpoint(portal, reply); WiFi.softAPdisconnect(true); assert(WiFi.live_mode == WIFI_STA && WiFi.initialized); WiFi.scans = WiFi.scan_deletes = DNSServer::starts = 0; WiFi.stale_picker_results = 2; } reset(); mesh::wireless::control().disabled = true; WebConfigServer disabled; char reply[160] = {}; assert(!disabled.startSetupMode(reply) && WiFi.ap_calls == 0 && !disabled._dns); // Promoting an existing LAN listener changes its network, not its lifetime. reset(); WiFi.shared_espnow = true; WebConfigServer promoted; promoted._mode = WebConfigServer::MODE_LAN; promoted._owns_wifi = false; promoted.servers = 1; assert(promoted.startSetupMode(reply)); requireEndpoint(promoted, reply); reset(Fault::StaleSsid); WiFi.shared_espnow = true; WebConfigServer failed_promotion; failed_promotion._mode = WebConfigServer::MODE_LAN; failed_promotion._owns_wifi = false; failed_promotion.servers = 1; assert(!failed_promotion.startSetupMode(reply)); assert(failed_promotion._mode == WebConfigServer::MODE_LAN); assert(failed_promotion.servers == 1 && !failed_promotion._dns); assert(!strstr(reply, "AP started") && DNSServer::starts == 0 && WiFi.scans == 1); assert(WiFi.initialized && WiFi.live_mode == WIFI_STA); // Existing setup, owned LAN, and pending teardown must reject a new start. for (int busy_case = 0; busy_case < 3; ++busy_case) { reset(); WebConfigServer busy; if (busy_case == 0) busy._mode = WebConfigServer::MODE_SETUP; else if (busy_case == 1) busy._mode = WebConfigServer::MODE_LAN; else busy._stopping = true; assert(!busy.startSetupMode(reply)); assert(strstr(reply, "busy") && WiFi.ap_calls == 0 && !busy._dns && busy.servers == 0); } // The first browser request reuses the completed pre-AP scan. Only an // explicit rescan starts scanning while the AP is serving clients. for (bool shared_espnow : {false, true}) { reset(); WiFi.shared_espnow = shared_espnow; WebConfigServer portal; assert(portal.startSetupMode(reply)); requireEndpoint(portal, reply); AsyncWebServerRequest scan; portal.handleScan(&scan); assert(scan.status == 200 && scan.payload == "{\"state\":\"done\"}" && scan.completed_networks == 2); assert(WiFi.scans == 1 && WiFi.scan_deletes == 1); assert(WiFi.departed_home_channel == !shared_espnow); assert(WiFi.scans_during_ap == 0); scan.rescan = true; portal.handleScan(&scan); assert(scan.status == 200 && scan.payload == "{\"state\":\"scanning\"}"); assert(WiFi.scans == 2 && WiFi.scan_deletes == 2 && WiFi.scans_during_ap == 1); portal.handleScan(&scan); assert(WiFi.scans == 2); // Existing request still owns the scan. delay(100); scan.rescan = false; portal.handleScan(&scan); assert(scan.payload == "{\"state\":\"done\"}" && scan.completed_networks == 2); assert(WiFi.scans == 2 && WiFi.scan_deletes == 2 && !WiFi.stale_picker_results); } // LAN mode has no AP startup pre-scan. Its first picker request must still // initiate scanning automatically when no results are available. for (bool shared_espnow : {false, true}) { reset(); WiFi.shared_espnow = shared_espnow; WiFi.scan_result = WIFI_SCAN_FAILED; WebConfigServer lan; lan._mode = WebConfigServer::MODE_LAN; lan._owns_wifi = false; lan.servers = 1; AsyncWebServerRequest scan; lan.handleScan(&scan); assert(scan.status == 200 && scan.payload == "{\"state\":\"scanning\"}"); assert(WiFi.scans == 1 && WiFi.sdk_scan_active && WiFi.scans_during_ap == 0); assert(WiFi.scan_deletes == 0 && WiFi.sdk_scan_stops == 0); lan.handleScan(&scan); assert(WiFi.scans == 1); delay(100); lan.handleScan(&scan); assert(scan.payload == "{\"state\":\"done\"}" && scan.completed_networks == 2); assert(WiFi.scans == 1 && !WiFi.sdk_scan_active); assert(lan._mode == WebConfigServer::MODE_LAN && lan.servers == 1 && !lan._dns); } reset(); WebConfigServer inactive; AsyncWebServerRequest inactive_request; inactive.handleScan(&inactive_request); assert(inactive_request.status == 503 && WiFi.scans == 0 && WiFi.scan_queries == 0); inactive._mode = WebConfigServer::MODE_SETUP; inactive.auth_allowed = false; inactive.handleScan(&inactive_request); assert(inactive_request.status == 401 && WiFi.scans == 0 && WiFi.scan_queries == 0); puts("WebConfig AP startup regression checks passed"); }