Fix V4 setup AP startup and pre-AP network scans
This commit is contained in:
1 parent
8d31b3932d
commit
2ee62b9814
14 files changed
+2233
-22
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# Preserve the byte-exact upstream source, including its original whitespace.
|
||||
WiFiScan.cpp -whitespace
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Pinned source witnesses for the build-local Arduino-ESP32 scan fix.
|
||||
|
||||
WiFiScan.cpp is the complete, byte-exact Arduino-ESP32 2.0.17 source, preserving
|
||||
its upstream copyright and LGPL notice:
|
||||
https://github.com/espressif/arduino-esp32/blob/2.0.17/libraries/WiFi/src/WiFiScan.cpp
|
||||
SHA256 e96ac4be873860dc2b441d6d01d6613eb4d06bb21e3f225c3f428619496e6c21
|
||||
The local .gitattributes preserves upstream whitespace for this hashed witness.
|
||||
|
||||
wifi_scan_config.h retains the original license notice and the exact scan type
|
||||
and configuration declarations from the pinned ESP-IDF 4.4.7 ESP32-S3 header:
|
||||
https://github.com/espressif/esp-idf/blob/v4.4.7/components/esp_wifi/include/esp_wifi_types.h
|
||||
This is a struct containing both active and passive dwell fields, not a union.
|
||||
|
||||
The test extracts and executes the actual scanNetworks method, capturing the
|
||||
configuration at its esp_wifi_scan_start boundary. Where supported, compiler
|
||||
automatic-variable poisoning makes the prior uninitialized fields deterministic.
|
||||
Older host compilers use explicit poison only for the unsafe negative control.
|
||||
The patched method is always executed unchanged. This proves initialization and
|
||||
preservation of scan parameters, not radio timing or physical AP discovery.
|
||||
|
||||
Arduino 3.1.3 and 3.3.11 are explicitly unchanged to preserve existing recipes.
|
||||
Their scan implementation and hardware behavior are outside this fix's scope.
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
ESP8266WiFiScan.cpp - WiFi library for esp8266
|
||||
|
||||
Copyright (c) 2014 Ivan Grokhotkov. All rights reserved.
|
||||
This file is part of the esp8266 core for Arduino environment.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Reworked on 28 Dec 2015 by Markus Sattler
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#include "WiFi.h"
|
||||
#include "WiFiGeneric.h"
|
||||
#include "WiFiScan.h"
|
||||
|
||||
extern "C" {
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <esp_err.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_event.h>
|
||||
#include <esp32-hal.h>
|
||||
#include <lwip/ip_addr.h>
|
||||
#include "lwip/err.h"
|
||||
}
|
||||
|
||||
bool WiFiScanClass::_scanAsync = false;
|
||||
uint32_t WiFiScanClass::_scanStarted = 0;
|
||||
uint32_t WiFiScanClass::_scanTimeout = 10000;
|
||||
uint16_t WiFiScanClass::_scanCount = 0;
|
||||
void* WiFiScanClass::_scanResult = 0;
|
||||
|
||||
/**
|
||||
* Start scan WiFi networks available
|
||||
* @param async run in async mode
|
||||
* @param show_hidden show hidden networks
|
||||
* @return Number of discovered networks
|
||||
*/
|
||||
int16_t WiFiScanClass::scanNetworks(bool async, bool show_hidden, bool passive, uint32_t max_ms_per_chan, uint8_t channel, const char * ssid, const uint8_t * bssid)
|
||||
{
|
||||
if(WiFiGenericClass::getStatusBits() & WIFI_SCANNING_BIT) {
|
||||
return WIFI_SCAN_RUNNING;
|
||||
}
|
||||
|
||||
WiFiScanClass::_scanTimeout = max_ms_per_chan * 20;
|
||||
WiFiScanClass::_scanAsync = async;
|
||||
|
||||
WiFi.enableSTA(true);
|
||||
|
||||
scanDelete();
|
||||
|
||||
wifi_scan_config_t config;
|
||||
config.ssid = (uint8_t*)ssid;
|
||||
config.bssid = (uint8_t*)bssid;
|
||||
config.channel = channel;
|
||||
config.show_hidden = show_hidden;
|
||||
if(passive){
|
||||
config.scan_type = WIFI_SCAN_TYPE_PASSIVE;
|
||||
config.scan_time.passive = max_ms_per_chan;
|
||||
} else {
|
||||
config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
|
||||
config.scan_time.active.min = 100;
|
||||
config.scan_time.active.max = max_ms_per_chan;
|
||||
}
|
||||
if(esp_wifi_scan_start(&config, false) == ESP_OK) {
|
||||
_scanStarted = millis();
|
||||
if (!_scanStarted) { //Prevent 0 from millis overflow
|
||||
++_scanStarted;
|
||||
}
|
||||
|
||||
WiFiGenericClass::clearStatusBits(WIFI_SCAN_DONE_BIT);
|
||||
WiFiGenericClass::setStatusBits(WIFI_SCANNING_BIT);
|
||||
|
||||
if(WiFiScanClass::_scanAsync) {
|
||||
return WIFI_SCAN_RUNNING;
|
||||
}
|
||||
if(WiFiGenericClass::waitStatusBits(WIFI_SCAN_DONE_BIT, 10000)){
|
||||
return (int16_t) WiFiScanClass::_scanCount;
|
||||
}
|
||||
}
|
||||
return WIFI_SCAN_FAILED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* private
|
||||
* scan callback
|
||||
* @param result void *arg
|
||||
* @param status STATUS
|
||||
*/
|
||||
void WiFiScanClass::_scanDone()
|
||||
{
|
||||
esp_wifi_scan_get_ap_num(&(WiFiScanClass::_scanCount));
|
||||
if(WiFiScanClass::_scanCount) {
|
||||
WiFiScanClass::_scanResult = new wifi_ap_record_t[WiFiScanClass::_scanCount];
|
||||
if(!WiFiScanClass::_scanResult || esp_wifi_scan_get_ap_records(&(WiFiScanClass::_scanCount), (wifi_ap_record_t*)_scanResult) != ESP_OK) {
|
||||
WiFiScanClass::_scanCount = 0;
|
||||
}
|
||||
}
|
||||
WiFiScanClass::_scanStarted=0; //Reset after a scan is completed for normal behavior
|
||||
WiFiGenericClass::setStatusBits(WIFI_SCAN_DONE_BIT);
|
||||
WiFiGenericClass::clearStatusBits(WIFI_SCANNING_BIT);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return bss_info *
|
||||
*/
|
||||
void * WiFiScanClass::_getScanInfoByIndex(int i)
|
||||
{
|
||||
if(!WiFiScanClass::_scanResult || (size_t) i >= WiFiScanClass::_scanCount) {
|
||||
return 0;
|
||||
}
|
||||
return reinterpret_cast<wifi_ap_record_t*>(WiFiScanClass::_scanResult) + i;
|
||||
}
|
||||
|
||||
/**
|
||||
* called to get the scan state in Async mode
|
||||
* @return scan result or status
|
||||
* -1 if scan not fin
|
||||
* -2 if scan not triggered
|
||||
*/
|
||||
int16_t WiFiScanClass::scanComplete()
|
||||
{
|
||||
if (WiFiScanClass::_scanStarted && (millis()-WiFiScanClass::_scanStarted) > WiFiScanClass::_scanTimeout) { //Check is scan was started and if the delay expired, return WIFI_SCAN_FAILED in this case
|
||||
WiFiGenericClass::clearStatusBits(WIFI_SCANNING_BIT);
|
||||
return WIFI_SCAN_FAILED;
|
||||
}
|
||||
|
||||
if(WiFiGenericClass::getStatusBits() & WIFI_SCAN_DONE_BIT) {
|
||||
return WiFiScanClass::_scanCount;
|
||||
}
|
||||
|
||||
if(WiFiGenericClass::getStatusBits() & WIFI_SCANNING_BIT) {
|
||||
return WIFI_SCAN_RUNNING;
|
||||
}
|
||||
|
||||
return WIFI_SCAN_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete last scan result from RAM
|
||||
*/
|
||||
void WiFiScanClass::scanDelete()
|
||||
{
|
||||
WiFiGenericClass::clearStatusBits(WIFI_SCAN_DONE_BIT);
|
||||
if(WiFiScanClass::_scanResult) {
|
||||
delete[] reinterpret_cast<wifi_ap_record_t*>(WiFiScanClass::_scanResult);
|
||||
WiFiScanClass::_scanResult = 0;
|
||||
WiFiScanClass::_scanCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* loads all infos from a scanned wifi in to the ptr parameters
|
||||
* @param networkItem uint8_t
|
||||
* @param ssid const char**
|
||||
* @param encryptionType uint8_t *
|
||||
* @param RSSI int32_t *
|
||||
* @param BSSID uint8_t **
|
||||
* @param channel int32_t *
|
||||
* @return (true if ok)
|
||||
*/
|
||||
bool WiFiScanClass::getNetworkInfo(uint8_t i, String &ssid, uint8_t &encType, int32_t &rssi, uint8_t* &bssid, int32_t &channel)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return false;
|
||||
}
|
||||
ssid = (const char*) it->ssid;
|
||||
encType = it->authmode;
|
||||
rssi = it->rssi;
|
||||
bssid = it->bssid;
|
||||
channel = it->primary;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the SSID discovered during the network scan.
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return ssid string of the specified item on the networks scanned list
|
||||
*/
|
||||
String WiFiScanClass::SSID(uint8_t i)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return String();
|
||||
}
|
||||
return String(reinterpret_cast<const char*>(it->ssid));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the encryption type of the networks discovered during the scanNetworks
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return encryption type (enum wl_enc_type) of the specified item on the networks scanned list
|
||||
*/
|
||||
wifi_auth_mode_t WiFiScanClass::encryptionType(uint8_t i)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return WIFI_AUTH_OPEN;
|
||||
}
|
||||
return it->authmode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the RSSI of the networks discovered during the scanNetworks
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return signed value of RSSI of the specified item on the networks scanned list
|
||||
*/
|
||||
int32_t WiFiScanClass::RSSI(uint8_t i)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return 0;
|
||||
}
|
||||
return it->rssi;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return MAC / BSSID of scanned wifi
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return uint8_t * MAC / BSSID of scanned wifi
|
||||
*/
|
||||
uint8_t * WiFiScanClass::BSSID(uint8_t i)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return 0;
|
||||
}
|
||||
return it->bssid;
|
||||
}
|
||||
|
||||
/**
|
||||
* return MAC / BSSID of scanned wifi
|
||||
* @param i specify from which network item want to get the information
|
||||
* @return String MAC / BSSID of scanned wifi
|
||||
*/
|
||||
String WiFiScanClass::BSSIDstr(uint8_t i)
|
||||
{
|
||||
char mac[18] = { 0 };
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return String();
|
||||
}
|
||||
sprintf(mac, "%02X:%02X:%02X:%02X:%02X:%02X", it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]);
|
||||
return String(mac);
|
||||
}
|
||||
|
||||
int32_t WiFiScanClass::channel(uint8_t i)
|
||||
{
|
||||
wifi_ap_record_t* it = reinterpret_cast<wifi_ap_record_t*>(_getScanInfoByIndex(i));
|
||||
if(!it) {
|
||||
return 0;
|
||||
}
|
||||
return it->primary;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
WIFI_SCAN_TYPE_ACTIVE = 0, /**< active scan */
|
||||
WIFI_SCAN_TYPE_PASSIVE, /**< passive scan */
|
||||
} wifi_scan_type_t;
|
||||
|
||||
/** @brief Range of active scan times per channel */
|
||||
typedef struct {
|
||||
uint32_t min; /**< minimum active scan time per channel, units: millisecond */
|
||||
uint32_t max; /**< maximum active scan time per channel, units: millisecond, values above 1500ms may
|
||||
cause station to disconnect from AP and are not recommended. */
|
||||
} wifi_active_scan_time_t;
|
||||
|
||||
/** @brief Aggregate of active & passive scan time per channel */
|
||||
typedef struct {
|
||||
wifi_active_scan_time_t active; /**< active scan time per channel, units: millisecond. */
|
||||
uint32_t passive; /**< passive scan time per channel, units: millisecond, values above 1500ms may
|
||||
cause station to disconnect from AP and are not recommended. */
|
||||
} wifi_scan_time_t;
|
||||
|
||||
/** @brief Parameters for an SSID scan. */
|
||||
typedef struct {
|
||||
uint8_t *ssid; /**< SSID of AP */
|
||||
uint8_t *bssid; /**< MAC address of AP */
|
||||
uint8_t channel; /**< channel, scan the specific channel */
|
||||
bool show_hidden; /**< enable to scan AP whose SSID is hidden */
|
||||
wifi_scan_type_t scan_type; /**< scan type, active or passive */
|
||||
wifi_scan_time_t scan_time; /**< scan time per channel */
|
||||
uint8_t home_chan_dwell_time;/**< time spent at home channel between scanning consecutive channels.*/
|
||||
} wifi_scan_config_t;
|
||||
Vendored
+600
@@ -0,0 +1,600 @@
|
||||
// Only peripheral endpoints are doubles. The readiness helper and startup
|
||||
// method are extracted verbatim from production WebConfigServer.cpp.
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
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<class... Args> 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<class T> 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; }
|
||||
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() {
|
||||
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");
|
||||
}
|
||||
@@ -1278,17 +1278,24 @@ uses_merged_standard_usb_logging esp_repeater \
|
||||
# were dead ends.
|
||||
verify_esp32_field_browser_ota() {
|
||||
local env_name=$1
|
||||
local minimal_lora_ota=${2:-0}
|
||||
local PLATFORMIO_BUILD_FLAGS=""
|
||||
local PLATFORMIO_BUILD_UNFLAGS=""
|
||||
local BUILD_PROFILE_FOR_TARGET=standard
|
||||
local ESP32_FULL_BUILD=0
|
||||
local MESHDEBUG_OVERRIDE=off
|
||||
local PACKET_LOGGING_OVERRIDE=off
|
||||
local MQTT_BRIDGE_OVERRIDE=off
|
||||
local FIRMWARE_FILENAME_INFIX=""
|
||||
local PIO_CONFIG_JSON='[]'
|
||||
local -a BUILD_CAPABILITIES=()
|
||||
local -a BUILD_REDUCTIONS=()
|
||||
local -a BUILD_EXPECTATIONS=()
|
||||
local capabilities reductions expectations
|
||||
local capabilities reductions expectations compact_flag
|
||||
|
||||
PIO_ENV_PLATFORM_BY_NAME[$env_name]=ESP32_PLATFORM
|
||||
PIO_ENV_OTA_BY_NAME[$env_name]=0
|
||||
PIO_ENV_OTA_BY_NAME[$env_name]=$minimal_lora_ota
|
||||
apply_lora_ota_override "$env_name"
|
||||
apply_esp32_lora_ota_size_profile "$env_name"
|
||||
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DLIGHTWEIGHT_WIFI_OTA=1* ]] \
|
||||
@@ -1308,11 +1315,46 @@ verify_esp32_field_browser_ota() {
|
||||
expectations=" ${BUILD_EXPECTATIONS[*]} "
|
||||
[[ "$expectations" == *"web.lightweight_browser_ota=MeshCore firmware update"* ]] \
|
||||
|| fail "$env_name does not verify browser OTA in the linked image"
|
||||
for compact_flag in ED25519_COMPACT_BASE ED25519_COMPACT_SHA512 OTA_TARGET_NAME_FRONT_CODED; do
|
||||
if [ "$minimal_lora_ota" = 1 ]; then
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-D${compact_flag}=1"* ]] \
|
||||
|| fail "$env_name did not enable its feature-preserving flash trim"
|
||||
else
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" != *"${compact_flag}"* ]] \
|
||||
|| fail "$env_name changed the ordinary standard implementation"
|
||||
fi
|
||||
done
|
||||
if [ "$minimal_lora_ota" = 1 ]; then
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DENABLE_OTA=1* \
|
||||
&& "$PLATFORMIO_BUILD_FLAGS" == *-DOTA_FLASH_STORE=1* \
|
||||
&& "$PLATFORMIO_BUILD_FLAGS" == *-DOTA_FOLDER_SERIAL* ]] \
|
||||
|| fail "$env_name removed a LoRa OTA or USB seeding feature to fit"
|
||||
[[ "$expectations" == *"ota.update.lora=image_hash MISMATCH after decode"* \
|
||||
&& "$expectations" == *"ota.cli=OTA: status"* ]] \
|
||||
|| fail "$env_name stopped verifying its LoRa OTA implementation"
|
||||
fi
|
||||
}
|
||||
|
||||
verify_esp32_field_browser_ota heltec_v4_repeater
|
||||
verify_esp32_field_browser_ota heltec_v4_room_server
|
||||
verify_esp32_field_browser_ota heltec_v4_sensor
|
||||
verify_esp32_field_browser_ota heltec_v4_repeater_lora_ota_no_external_sensors 1
|
||||
verify_esp32_field_browser_ota heltec_v4_room_server_lora_ota_no_external_sensors 1
|
||||
|
||||
# The new storage representation is confined to portable ESP32 minimal OTA
|
||||
# artifacts. Full images and other platforms retain their existing policies.
|
||||
(
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_UNFLAGS=""
|
||||
BUILD_PROFILE_FOR_TARGET=full
|
||||
ESP32_FULL_BUILD=1
|
||||
apply_esp32_lora_ota_size_profile heltec_v4_repeater_lora_ota_no_external_sensors
|
||||
[ -z "$PLATFORMIO_BUILD_FLAGS" ] || fail "Full image received portable flash trims"
|
||||
BUILD_PROFILE_FOR_TARGET=standard
|
||||
ESP32_FULL_BUILD=0
|
||||
apply_esp32_lora_ota_size_profile nrf_repeater_lora_ota_no_external_sensors
|
||||
[ -z "$PLATFORMIO_BUILD_FLAGS" ] || fail "nRF52 image received ESP32 portable flash trims"
|
||||
)
|
||||
|
||||
# Personal/attached roles can retain their established transport policy; the
|
||||
# fail-safe is deliberately scoped to remotely installed field/server images.
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the pinned scan method with poisoned locals at its SDK boundary."""
|
||||
import configparser
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURES = ROOT / "test/fixtures/esp32_wifi_scan"
|
||||
SPEC = importlib.util.spec_from_file_location("wifi_scan_fix", ROOT / "scripts/esp32_wifi_scan_fix.py")
|
||||
FIX = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(FIX)
|
||||
RAW = (FIXTURES / "WiFiScan.cpp").read_text(encoding="utf-8")
|
||||
PATCHED = FIX.patched_scan_source(RAW)
|
||||
VERSION = ROOT / "test/fixtures/esp32_file_buffer/esp_arduino_version.h"
|
||||
|
||||
|
||||
def scan_method(source):
|
||||
start = source.index("int16_t WiFiScanClass::scanNetworks(")
|
||||
opening = source.index("{", start)
|
||||
depth = 0
|
||||
for end in range(opening, len(source)):
|
||||
depth += (source[end] == "{") - (source[end] == "}")
|
||||
if depth == 0:
|
||||
return source[start:end + 1]
|
||||
raise AssertionError("unclosed scan method")
|
||||
|
||||
|
||||
PREFIX = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include "wifi_scan_config.h"
|
||||
static constexpr int ESP_OK = 0;
|
||||
static constexpr int WIFI_SCAN_RUNNING = -1, WIFI_SCAN_FAILED = -2;
|
||||
static constexpr uint32_t WIFI_SCANNING_BIT = 1, WIFI_SCAN_DONE_BIT = 2;
|
||||
static uint32_t status_bits = 0;
|
||||
static unsigned enabled_calls = 0, deleted_calls = 0, sdk_calls = 0;
|
||||
static wifi_scan_config_t captured;
|
||||
static uint32_t millis() { return 42; }
|
||||
struct WiFiGenericClass {
|
||||
static uint32_t getStatusBits() { return status_bits; }
|
||||
static void clearStatusBits(uint32_t bits) { status_bits &= ~bits; }
|
||||
static void setStatusBits(uint32_t bits) { status_bits |= bits; }
|
||||
static uint32_t waitStatusBits(uint32_t bits, uint32_t) { return status_bits & bits; }
|
||||
};
|
||||
struct WiFiMock { void enableSTA(bool enabled) { assert(enabled); ++enabled_calls; } } WiFi;
|
||||
struct WiFiScanClass {
|
||||
static uint32_t _scanTimeout, _scanStarted;
|
||||
static bool _scanAsync;
|
||||
static uint16_t _scanCount;
|
||||
static void scanDelete() { ++deleted_calls; }
|
||||
int16_t scanNetworks(bool, bool, bool, uint32_t, uint8_t, const char*, const uint8_t*);
|
||||
};
|
||||
uint32_t WiFiScanClass::_scanTimeout = 0, WiFiScanClass::_scanStarted = 0;
|
||||
bool WiFiScanClass::_scanAsync = false;
|
||||
uint16_t WiFiScanClass::_scanCount = 0;
|
||||
static int esp_wifi_scan_start(const wifi_scan_config_t* config, bool block) {
|
||||
assert(!block);
|
||||
captured = *config;
|
||||
++sdk_calls;
|
||||
return ESP_OK;
|
||||
}
|
||||
// GCC 11 has no automatic-local poisoning flag. The negative control alone
|
||||
// models a poisoned prior stack in that case; production initialization stays
|
||||
// unchanged. All fields interpreted at the SDK boundary are then overwritten
|
||||
// by the original method or consist of unsigned integers.
|
||||
static wifi_scan_config_t poisonedScanConfig() {
|
||||
wifi_scan_config_t config;
|
||||
std::memset(&config, 0xa5, sizeof(config));
|
||||
return config;
|
||||
}
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
int main() {
|
||||
WiFiScanClass scanner;
|
||||
const char ssid[] = "filter";
|
||||
const uint8_t bssid[] = {1, 2, 3, 4, 5, 6};
|
||||
for (bool passive : {false, true}) {
|
||||
for (uint32_t dwell : {120u, 200u, 300u}) {
|
||||
status_bits = 0;
|
||||
assert(scanner.scanNetworks(true, true, passive, dwell, 6, ssid, bssid)
|
||||
== WIFI_SCAN_RUNNING);
|
||||
assert(captured.ssid == reinterpret_cast<const uint8_t*>(ssid));
|
||||
assert(captured.bssid == bssid && captured.channel == 6 && captured.show_hidden);
|
||||
assert(captured.scan_type == (passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE));
|
||||
assert(WiFiScanClass::_scanTimeout == dwell * 20 && WiFiScanClass::_scanStarted == 42);
|
||||
assert((status_bits & WIFI_SCANNING_BIT) && !(status_bits & WIFI_SCAN_DONE_BIT));
|
||||
if (captured.home_chan_dwell_time != 0
|
||||
|| (passive && (captured.scan_time.active.min != 0 || captured.scan_time.active.max != 0))
|
||||
|| (!passive && captured.scan_time.passive != 0)) {
|
||||
std::puts("scan defaults were not zeroed");
|
||||
return 1;
|
||||
}
|
||||
if (passive) assert(captured.scan_time.passive == dwell);
|
||||
else assert(captured.scan_time.active.min == 100 && captured.scan_time.active.max == dwell);
|
||||
}
|
||||
}
|
||||
assert(enabled_calls == 6 && deleted_calls == 6 && sdk_calls == 6);
|
||||
// An existing scan must return without starting or modifying another scan.
|
||||
assert(scanner.scanNetworks(true, false, false, 200, 0, nullptr, nullptr) == WIFI_SCAN_RUNNING);
|
||||
assert(sdk_calls == 6 && enabled_calls == 6 && deleted_calls == 6);
|
||||
status_bits = 0;
|
||||
assert(scanner.scanNetworks(true, false, false, 200, 0, nullptr, nullptr) == WIFI_SCAN_RUNNING);
|
||||
assert(!captured.ssid && !captured.bssid && captured.channel == 0 && !captured.show_hidden);
|
||||
std::puts("WiFi scan SDK configuration checks passed");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, path): self.path = path
|
||||
def srcnode(self): return self
|
||||
def get_abspath(self): return str(self.path)
|
||||
|
||||
|
||||
class Env(dict):
|
||||
def __init__(self, defines, build):
|
||||
super().__init__(CPPDEFINES=defines)
|
||||
self.build, self.paths, self.middleware = build, [], []
|
||||
def subst(self, value):
|
||||
assert value == "$BUILD_DIR"
|
||||
return str(self.build)
|
||||
def File(self, path): return path
|
||||
def AppendUnique(self, **kwargs):
|
||||
for path in kwargs["CPPPATH"]:
|
||||
if path not in self.paths: self.paths.append(path)
|
||||
def AddBuildMiddleware(self, callback, pattern): self.middleware.append((callback, pattern))
|
||||
|
||||
|
||||
class WiFiScanFixTest(unittest.TestCase):
|
||||
def test_actual_scan_method_zeroes_unused_fields_with_poisoned_negative_control(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("c++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "A host C++11 compiler is required")
|
||||
probe = subprocess.run([compiler, "-x", "c++", "-std=c++11",
|
||||
"-ftrivial-auto-var-init=pattern", "-fsyntax-only", "-"],
|
||||
input="int main() {}\n", capture_output=True, text=True, timeout=30)
|
||||
poison_flags = ["-ftrivial-auto-var-init=pattern"] if probe.returncode == 0 else []
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-wifi-scan-sdk-") as directory:
|
||||
temp = Path(directory)
|
||||
shutil.copyfile(FIXTURES / "wifi_scan_config.h", temp / "wifi_scan_config.h")
|
||||
for source, expected in ((PATCHED, 0), (RAW, 1)):
|
||||
with self.subTest(patched=expected == 0):
|
||||
method = scan_method(source)
|
||||
if expected and not poison_flags:
|
||||
method = method.replace(FIX.ORIGINAL_DECLARATION,
|
||||
"wifi_scan_config_t config = poisonedScanConfig();", 1)
|
||||
generated = temp / "scan.cpp"
|
||||
generated.write_text(PREFIX + "\n#include <initializer_list>\n"
|
||||
+ method + MAIN, encoding="utf-8")
|
||||
binary = temp / "scan.exe"
|
||||
built = subprocess.run([compiler, "-std=c++11", "-O1", "-Wall", "-Wextra",
|
||||
"-Werror", "-Wno-unused-function", *poison_flags,
|
||||
str(generated), "-o", str(binary)], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, expected, checked.stdout + checked.stderr)
|
||||
self.assertIn("WiFi scan SDK configuration checks passed" if expected == 0
|
||||
else "scan defaults were not zeroed", checked.stdout)
|
||||
|
||||
def test_exact_source_pin_transform_idempotence_and_changed_source_rejection(self):
|
||||
self.assertEqual(hashlib.sha256((FIXTURES / "WiFiScan.cpp").read_bytes()).hexdigest(),
|
||||
FIX.PINNED_SCAN_SHA256)
|
||||
self.assertEqual(hashlib.sha256((FIXTURES / "wifi_scan_config.h").read_bytes()).hexdigest(),
|
||||
"9b25412ea07a0a8fe295d02c9f0ddaee2bc984397483d5d56db7e54b5aacf2ce")
|
||||
self.assertEqual(PATCHED, RAW.replace(FIX.ORIGINAL_DECLARATION, FIX.ZEROED_DECLARATION, 1))
|
||||
self.assertEqual(FIX.patched_scan_source(RAW.replace("\n", "\r\n")), PATCHED)
|
||||
self.assertEqual(FIX.patched_scan_source(PATCHED), PATCHED)
|
||||
for changed in (RAW + "\n", PATCHED + "\n", RAW.replace("active.min = 100", "active.min = 50")):
|
||||
with self.assertRaises(RuntimeError): FIX.patched_scan_source(changed)
|
||||
for version in ((2, 0, 18), (3, 1, 3), (3, 3, 11), (3, 3, 12)):
|
||||
with self.assertRaises(RuntimeError): FIX.patched_scan_source(RAW, version)
|
||||
|
||||
def test_private_copy_preserves_shared_framework_and_is_idempotent(self):
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-wifi-scan-copy-") as directory:
|
||||
temp = Path(directory)
|
||||
source = temp / "sdk/libraries/WiFi/src/WiFiScan.cpp"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_bytes((FIXTURES / "WiFiScan.cpp").read_bytes())
|
||||
header = temp / "sdk/cores/esp32/esp_arduino_version.h"
|
||||
header.parent.mkdir(parents=True)
|
||||
header.write_bytes(VERSION.read_bytes())
|
||||
node = Node(source)
|
||||
for defines in ([], ["RP2040_PLATFORM"], [("ESP32_PLATFORM", 0)]):
|
||||
env = Env(defines, temp / "untouched")
|
||||
self.assertIs(FIX.replace_scan_source(env, node), node)
|
||||
self.assertFalse(env.build.exists())
|
||||
for index, defines in enumerate((["ESP32_PLATFORM"], [("ESP32", 1)],
|
||||
["ESP32_PLATFORM", ("ARDUINO_USB_MODE", 0)],
|
||||
["ESP32_PLATFORM", ("ARDUINO_USB_MODE", 1)])):
|
||||
env = Env(defines, temp / ("build" + str(index)))
|
||||
result = Path(FIX.replace_scan_source(env, node))
|
||||
self.assertEqual(result.read_text(encoding="utf-8"), PATCHED)
|
||||
self.assertEqual(result, env.build / "patched-esp32-wifi/WiFiScan.cpp")
|
||||
self.assertEqual(env.paths, [str(source.parent)])
|
||||
self.assertEqual(source.read_bytes(), (FIXTURES / "WiFiScan.cpp").read_bytes())
|
||||
self.assertEqual(header.read_bytes(), VERSION.read_bytes())
|
||||
before = result.stat().st_mtime_ns
|
||||
self.assertEqual(Path(FIX.replace_scan_source(env, node)).stat().st_mtime_ns, before)
|
||||
env = Env(["ESP32"], temp / "registration")
|
||||
unrelated = Node(temp / "libraries/WiFi/src/Other.cpp")
|
||||
self.assertIs(FIX.replace_scan_source(env, unrelated), unrelated)
|
||||
FIX.install(env)
|
||||
self.assertEqual(env.middleware,
|
||||
[(FIX.replace_scan_source, "*libraries*WiFi*src*WiFiScan.cpp")])
|
||||
|
||||
def test_known_arduino3_is_unchanged_unknown_or_malformed_framework_fails_closed(self):
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-wifi-scan-version-") as directory:
|
||||
temp = Path(directory)
|
||||
source = temp / "sdk/libraries/WiFi/src/WiFiScan.cpp"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text("Arduino 3 source is outside this narrow patch", encoding="utf-8")
|
||||
header = temp / "sdk/cores/esp32/esp_arduino_version.h"
|
||||
header.parent.mkdir(parents=True)
|
||||
env, node = Env(["ESP32_PLATFORM"], temp / "build"), Node(source)
|
||||
def version_text(version):
|
||||
return "".join(f"#define ESP_ARDUINO_VERSION_{key} {value}\n"
|
||||
for key, value in zip(("MAJOR", "MINOR", "PATCH"), version))
|
||||
for version in ((3, 1, 3), (3, 3, 11)):
|
||||
header.write_text(version_text(version))
|
||||
self.assertEqual(FIX.require_known_framework(source), version)
|
||||
self.assertIs(FIX.replace_scan_source(env, node), node)
|
||||
self.assertFalse(env.build.exists())
|
||||
for changed in ("", version_text((2, 0, 18)), version_text((3, 3, 12)),
|
||||
version_text((2, 0, 17)) + "#define ESP_ARDUINO_VERSION_MAJOR 2\n"):
|
||||
header.write_text(changed)
|
||||
with self.assertRaises(RuntimeError): FIX.replace_scan_source(env, node)
|
||||
self.assertFalse(env.build.exists())
|
||||
header.unlink()
|
||||
with self.assertRaises(RuntimeError): FIX.replace_scan_source(env, node)
|
||||
header.write_bytes(VERSION.read_bytes())
|
||||
with self.assertRaises(RuntimeError): FIX.replace_scan_source(env, node)
|
||||
self.assertFalse(env.build.exists())
|
||||
|
||||
def test_common_esp32_registration(self):
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
config.read(ROOT / "platformio.ini")
|
||||
self.assertIn("pre:scripts/esp32_wifi_scan_fix.py", config["esp32_base"]["extra_scripts"].split())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise production WebConfig AP startup against independent SDK state."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src/helpers/esp32/WebConfigServer.cpp"
|
||||
FIXTURE = ROOT / "test/fixtures/webconfig_ap_start.cpp"
|
||||
|
||||
|
||||
class WebConfigApStartTest(unittest.TestCase):
|
||||
def test_live_ap_state_and_reusable_handoff(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "A host C++17 compiler is required")
|
||||
source = SOURCE.read_text(encoding="utf-8")
|
||||
# Keeping helper extraction optional lets the old implementation compile:
|
||||
# its false-success behavior must fail executable checks, not compilation.
|
||||
helper = ""
|
||||
if "static bool setupAccessPointStarted(" in source:
|
||||
helper += extract_braced(source, "static bool setupAccessPointStarted(") + "\n"
|
||||
if "static bool setupAccessPointReady(" in source:
|
||||
helper += extract_braced(source, "static bool setupAccessPointReady(") + "\n"
|
||||
if "static bool scanSetupNetworks(" in source:
|
||||
helper += extract_braced(source, "static bool scanSetupNetworks(")
|
||||
method = extract_braced(source, "bool WebConfigServer::startSetupMode(")
|
||||
scan_handler = extract_braced(source, "void WebConfigServer::handleScan(")
|
||||
policy = (ROOT / "src/helpers/esp32/WiFiRadioPolicy.h").read_text(encoding="utf-8")
|
||||
mask = policy[policy.index("static constexpr uint8_t kAccessPointProtocolMask ="):]
|
||||
mask = mask[:mask.index(";") + 1]
|
||||
fixture = FIXTURE.read_text(encoding="utf-8").replace("@AP_MASK@", mask)
|
||||
fixture = fixture.replace("@METHODS@", helper + "\n" + method + "\n" + scan_handler)
|
||||
with tempfile.TemporaryDirectory(prefix="webconfig-ap-start-") as directory:
|
||||
work = Path(directory)
|
||||
generated = work / "fixture.cpp"
|
||||
generated.write_text(fixture, encoding="utf-8")
|
||||
binary = work / "webconfig-ap-start.exe"
|
||||
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
||||
for arduino_major, protected in ((2, False), (2, True), (3, False), (3, True)):
|
||||
with self.subTest(arduino_major=arduino_major, protected=protected):
|
||||
security_flags = ['-DWEBCONFIG_AP_PASSWORD="testpass"'] if protected else []
|
||||
compiled = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
*sanitizer, f"-DESP_ARDUINO_VERSION_MAJOR={arduino_major}",
|
||||
*security_flags,
|
||||
str(generated), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True,
|
||||
text=True, timeout=15)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn("WebConfig AP startup regression checks passed", checked.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user