Keep ESP32 access points compatible with LR transports
Protect standard access points from the shared ESP-IDF LR flag, defer incompatible raw ESP-NOW starts, and select RAM storage before LR writes. Track successful LR owners so setup and OTA access points reject a conflicting running transport before changing WiFi. Run actual production policies, transport lifecycle methods, and WebConfig teardown branches against a shared-LR SDK model with runtime negative controls; include the regression in unit CI.
This commit is contained in:
1 parent
526af2a82b
commit
8c7d7d8839
17 files changed
+557
-17
No files matched your search
@@ -367,6 +367,7 @@ jobs:
|
||||
python3 -B test/test_serial_ethernet_sessions.py -v
|
||||
python3 -B test/test_companion_wifi_session.py -v
|
||||
python3 -B test/test_espnow_radio_lifecycle.py -v
|
||||
python3 -B test/test_wifi_ap_lr_safety.py -v
|
||||
python3 -B test/test_espnow_runtime.py -v
|
||||
python3 -B test/test_wifi_ota_lifecycle.py -v
|
||||
|
||||
|
||||
@@ -299,6 +299,11 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
if (!use_ap) {
|
||||
ip = WiFi.localIP();
|
||||
} else {
|
||||
if (!mesh::wifi::accessPointCompatibleWithLongRange()) {
|
||||
inhibit_sleep = ota_server != nullptr;
|
||||
strcpy(reply, "ERR: stop ESP-NOW before starting an OTA AP");
|
||||
return false;
|
||||
}
|
||||
ota_started_radio = ota_started_radio || WiFi.getMode() == WIFI_OFF;
|
||||
ota_started_ap = mesh::wifi::startOpenAccessPoint("MeshCore-OTA", ota_started_ap);
|
||||
if (!ota_started_ap) {
|
||||
@@ -397,6 +402,11 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
if (!use_ap) {
|
||||
ip = WiFi.localIP();
|
||||
} else {
|
||||
if (!mesh::wifi::accessPointCompatibleWithLongRange()) {
|
||||
inhibit_sleep = ota_server != nullptr;
|
||||
strcpy(reply, "ERR: stop ESP-NOW before starting an OTA AP");
|
||||
return false;
|
||||
}
|
||||
ota_started_radio = ota_started_radio || WiFi.getMode() == WIFI_OFF;
|
||||
ota_started_ap = mesh::wifi::startOpenAccessPoint("MeshCore-OTA", ota_started_ap);
|
||||
if (!ota_started_ap) {
|
||||
|
||||
@@ -80,6 +80,11 @@ void ESPNowBridge::begin() {
|
||||
|
||||
_active_format = mesh::bridge::isValidEspNowFormat(_prefs->bridge_format)
|
||||
? _prefs->bridge_format : mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
||||
const bool raw_format = _active_format == mesh::bridge::ESPNOW_FORMAT_RAW;
|
||||
if (raw_format && mesh::wifi::checkLongRangeRadioStart() != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("ESP-NOW LR start deferred while a WiFi AP is active\n");
|
||||
return;
|
||||
}
|
||||
portENTER_CRITICAL(&_rx_mux);
|
||||
_rx_head = 0;
|
||||
_rx_tail = 0;
|
||||
@@ -126,14 +131,12 @@ void ESPNowBridge::begin() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool raw_format =
|
||||
_active_format == mesh::bridge::ESPNOW_FORMAT_RAW;
|
||||
if (raw_format) {
|
||||
// Primary ESPNOWRadio nodes use Espressif's LR PHY. Retain B/G/N receive
|
||||
// support as well, but force this peer's outgoing frames to LR below.
|
||||
const uint8_t protocols = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G
|
||||
| WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR;
|
||||
if (esp_wifi_set_protocol(WIFI_IF_STA, protocols) != ESP_OK) {
|
||||
if (mesh::wifi::applyStationProtocolMask(protocols, true) != ESP_OK) {
|
||||
BRIDGE_DEBUG_PRINTLN("Error enabling ESP-NOW LR protocol\n");
|
||||
stopBridgeWiFiIfUnused();
|
||||
return;
|
||||
@@ -207,6 +210,7 @@ void ESPNowBridge::begin() {
|
||||
|
||||
// Update bridge state
|
||||
_initialized = true;
|
||||
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeBridgeOwner, raw_format);
|
||||
mesh::wifi::bridgeEspNowChannel().store(_prefs->bridge_channel);
|
||||
}
|
||||
|
||||
@@ -231,6 +235,7 @@ void ESPNowBridge::end() {
|
||||
|
||||
// Infrastructure WiFi may still own this shared radio.
|
||||
mesh::wifi::bridgeEspNowChannel().store(0);
|
||||
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeBridgeOwner, false);
|
||||
stopBridgeWiFiIfUnused();
|
||||
|
||||
portENTER_CRITICAL(&_rx_mux);
|
||||
|
||||
@@ -81,6 +81,12 @@ static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
|
||||
|
||||
void ESPNOWRadio::init() {
|
||||
if (initialized_ || mesh::wireless::control().blocked(mesh::wireless::EspNow)) return;
|
||||
// A conventional AP and the LR PHY share one enable flag on IDF 4.4. Do not
|
||||
// hide the AP or claim a successful new LR start while AP requires B/G/N.
|
||||
if (mesh::wifi::checkLongRangeRadioStart() != ESP_OK) {
|
||||
ESPNOW_DEBUG_PRINTLN("ESP-NOW LR start deferred while a WiFi AP is active");
|
||||
return;
|
||||
}
|
||||
portENTER_CRITICAL(&rx_mux);
|
||||
rx_head = 0;
|
||||
rx_tail = 0;
|
||||
@@ -112,13 +118,13 @@ void ESPNOWRadio::init() {
|
||||
// access points while retaining LR for ESP-NOW, and pin both transports to
|
||||
// one channel because an ESP32 cannot associate and send ESP-NOW on two
|
||||
// different channels simultaneously.
|
||||
if (mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK) {
|
||||
if (mesh::wifi::applyStationProtocolMask(mesh::wifi::kProtocolMask, true) != ESP_OK) {
|
||||
ESPNOW_DEBUG_PRINTLN("Error configuring ESP-NOW/WiFi coexistence");
|
||||
return;
|
||||
}
|
||||
#else
|
||||
// Dedicated ESP-NOW targets retain the original LR-only behavior.
|
||||
if (esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR) != ESP_OK) {
|
||||
if (mesh::wifi::applyStationProtocolMask(WIFI_PROTOCOL_LR, true) != ESP_OK) {
|
||||
ESPNOW_DEBUG_PRINTLN("Error configuring ESP-NOW LR protocol");
|
||||
return;
|
||||
}
|
||||
@@ -203,6 +209,7 @@ void ESPNOWRadio::init() {
|
||||
#endif
|
||||
#endif
|
||||
accepting_packets.store(true);
|
||||
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeRadioOwner, true);
|
||||
ESPNOW_DEBUG_PRINTLN("init success");
|
||||
} else {
|
||||
end();
|
||||
@@ -212,6 +219,7 @@ void ESPNOWRadio::init() {
|
||||
void ESPNOWRadio::end() {
|
||||
if (send_active_) send_cancelled_ = true;
|
||||
accepting_packets.store(false);
|
||||
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeRadioOwner, false);
|
||||
if (initialized_) {
|
||||
esp_now_unregister_send_cb();
|
||||
esp_now_unregister_recv_cb();
|
||||
|
||||
@@ -802,6 +802,10 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
strcpy(reply, "Error: WiFi disabled; use set wifi on or set 2.4ghz on");
|
||||
return false;
|
||||
}
|
||||
if (!mesh::wifi::accessPointCompatibleWithLongRange()) {
|
||||
strcpy(reply, "Error: stop ESP-NOW before starting a setup AP");
|
||||
return false;
|
||||
}
|
||||
const bool promote_lan = _mode == MODE_LAN && !_owns_wifi && !_stopping;
|
||||
if ((_mode != MODE_OFF && !promote_lan) || _stopping) {
|
||||
strcpy(reply, "Err: webconfig busy");
|
||||
@@ -1208,7 +1212,8 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_dns = NULL;
|
||||
}
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_STA);
|
||||
if (!WiFi.mode(WIFI_STA)
|
||||
|| mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK) return;
|
||||
mesh::wifi::setStationAutoReconnect(true);
|
||||
_was_setup_ap = false;
|
||||
_initial_setup = false;
|
||||
@@ -1302,7 +1307,8 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_dns = NULL;
|
||||
}
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_STA);
|
||||
if (!WiFi.mode(WIFI_STA)
|
||||
|| mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK) return;
|
||||
mesh::wifi::setStationAutoReconnect(true);
|
||||
_was_setup_ap = false;
|
||||
_initial_setup = false;
|
||||
|
||||
@@ -59,6 +59,7 @@ inline bool scanBeforeAccessPoint() {
|
||||
}
|
||||
|
||||
inline bool startOpenAccessPoint(const char* ssid, bool reuse_existing) {
|
||||
if (!accessPointCompatibleWithLongRange()) return false;
|
||||
const int channel = accessPointChannel();
|
||||
// Repeated OTA commands retain joined clients when the AP is already live.
|
||||
if (reuse_existing && applyAccessPointProtocolMask() == ESP_OK
|
||||
@@ -96,6 +97,12 @@ inline bool startOpenAccessPoint(const char* ssid, bool reuse_existing) {
|
||||
}
|
||||
|
||||
inline void stopTemporaryAccessPointRadio(bool started_radio) {
|
||||
// Removing AP releases its shared LR constraint. The station policy checks
|
||||
// the live SDK mode and cannot re-enable LR while AP is still present.
|
||||
if (espNowChannelConstrained()) {
|
||||
applyProtocolMask(WIFI_IF_STA);
|
||||
return;
|
||||
}
|
||||
// Only restore OFF when this AP initialized the radio. A station inherited
|
||||
// from WebConfig/MQTT belongs to the caller's service ownership policy.
|
||||
if (started_radio && !espNowChannelConstrained()
|
||||
|
||||
@@ -41,11 +41,26 @@ static constexpr uint8_t kProtocolMask = WIFI_PROTOCOL_11B
|
||||
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N;
|
||||
#endif
|
||||
|
||||
// LR is private to ESP-NOW on the station interface. A setup SoftAP must keep
|
||||
// an ordinary b/g/n protocol bitmap so phones and laptops can discover it.
|
||||
// ESP-IDF 4.4 shares the LR enable flag between STA and AP. A later STA LR
|
||||
// write can therefore change AP beacons even after AP was configured as B/G/N.
|
||||
// Conventional setup access points take precedence while they are enabled.
|
||||
static constexpr uint8_t kAccessPointProtocolMask = WIFI_PROTOCOL_11B
|
||||
| WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N;
|
||||
|
||||
static constexpr uint8_t kLongRangeRadioOwner = 1;
|
||||
static constexpr uint8_t kLongRangeBridgeOwner = 2;
|
||||
inline std::atomic<uint8_t>& longRangeOwners() {
|
||||
static std::atomic<uint8_t> owners{0};
|
||||
return owners;
|
||||
}
|
||||
inline void setLongRangeOwner(uint8_t owner, bool active) {
|
||||
if (active) longRangeOwners().fetch_or(owner);
|
||||
else longRangeOwners().fetch_and(static_cast<uint8_t>(~owner));
|
||||
}
|
||||
inline bool accessPointCompatibleWithLongRange() {
|
||||
return longRangeOwners().load() == 0;
|
||||
}
|
||||
|
||||
// A bridge can start/stop independently of infrastructure WiFi. Its channel
|
||||
// constraint exists only while it runs; primary ESP-NOW keeps its boot policy.
|
||||
inline std::atomic<uint8_t>& bridgeEspNowChannel() {
|
||||
@@ -115,14 +130,69 @@ inline bool saveConfiguredEspNowChannel(uint8_t) {
|
||||
}
|
||||
#endif
|
||||
|
||||
inline esp_err_t applyProtocolMask(wifi_interface_t interface_id) {
|
||||
return esp_wifi_set_protocol(interface_id, kProtocolMask);
|
||||
// Check the SDK rather than Arduino's facade: an ESP-NOW bridge can own a
|
||||
// driver which Arduino has not initialized. An absent driver is safe to start.
|
||||
inline esp_err_t checkLongRangeRadioStart() {
|
||||
wifi_mode_t mode = WIFI_MODE_NULL;
|
||||
const esp_err_t result = esp_wifi_get_mode(&mode);
|
||||
if (result == ESP_ERR_WIFI_NOT_INIT) return ESP_OK;
|
||||
if (result != ESP_OK) return result;
|
||||
return (mode & WIFI_MODE_AP) ? ESP_ERR_INVALID_STATE : ESP_OK;
|
||||
}
|
||||
|
||||
inline esp_err_t applyAccessPointProtocolMask() {
|
||||
return esp_wifi_set_protocol(WIFI_IF_AP, kAccessPointProtocolMask);
|
||||
}
|
||||
|
||||
inline esp_err_t applyStationProtocolMask(uint8_t protocols, bool require_lr = false) {
|
||||
wifi_mode_t mode = WIFI_MODE_NULL;
|
||||
esp_err_t result = esp_wifi_get_mode(&mode);
|
||||
if (result != ESP_OK) return result;
|
||||
if ((mode & WIFI_MODE_AP) && require_lr) return ESP_ERR_INVALID_STATE;
|
||||
if (mode & WIFI_MODE_AP) protocols = kAccessPointProtocolMask;
|
||||
|
||||
// persistent(false) only changes Arduino's next initialization preference.
|
||||
// Select RAM on this already initialized SDK driver before any LR write.
|
||||
if (protocols & WIFI_PROTOCOL_LR) {
|
||||
result = esp_wifi_set_storage(WIFI_STORAGE_RAM);
|
||||
if (result != ESP_OK) return result;
|
||||
}
|
||||
result = esp_wifi_set_protocol(WIFI_IF_STA, protocols);
|
||||
if (result != ESP_OK) return result;
|
||||
|
||||
// Recheck after the write so an AP enabled by another owner is repaired too.
|
||||
result = esp_wifi_get_mode(&mode);
|
||||
if (result != ESP_OK) {
|
||||
// Unknown mode is not proof that LR is safe. Best-effort B/G/N rollback
|
||||
// also clears the shared LR flag if an AP appeared during this write.
|
||||
// An established LR owner already prevents AP startup. Do not silently
|
||||
// downgrade that owner when a later infrastructure-only mode read fails.
|
||||
if (accessPointCompatibleWithLongRange()) {
|
||||
esp_wifi_set_protocol(WIFI_IF_STA, kAccessPointProtocolMask);
|
||||
applyAccessPointProtocolMask();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (mode & WIFI_MODE_AP) {
|
||||
result = applyAccessPointProtocolMask();
|
||||
if (result != ESP_OK) {
|
||||
esp_wifi_set_protocol(WIFI_IF_STA, kAccessPointProtocolMask);
|
||||
return result;
|
||||
}
|
||||
uint8_t actual = 0;
|
||||
result = esp_wifi_get_protocol(WIFI_IF_AP, &actual);
|
||||
if (result != ESP_OK) return result;
|
||||
if (actual != kAccessPointProtocolMask || require_lr) return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
inline esp_err_t applyProtocolMask(wifi_interface_t interface_id) {
|
||||
return interface_id == WIFI_IF_STA
|
||||
? applyStationProtocolMask(kProtocolMask)
|
||||
: esp_wifi_set_protocol(interface_id, kProtocolMask);
|
||||
}
|
||||
|
||||
// An ESP-NOW radio and an associated station cannot occupy different channels.
|
||||
// Full Companion therefore keeps both its setup AP and any infrastructure-WiFi
|
||||
// association on the mesh channel. Passing zero on ordinary targets preserves
|
||||
|
||||
+4
@@ -141,6 +141,10 @@ struct Facade {
|
||||
} WiFi;
|
||||
|
||||
namespace mesh { namespace wifi {
|
||||
constexpr uint8_t kLongRangeBridgeOwner=2;
|
||||
void setLongRangeOwner(uint8_t,bool){}
|
||||
int checkLongRangeRadioStart() { return sdk_initialized && (sdk_mode & WIFI_AP) ? -1 : ESP_OK; }
|
||||
int applyStationProtocolMask(uint8_t mask, bool) { return esp_wifi_set_protocol(WIFI_IF_STA, mask); }
|
||||
std::atomic<uint8_t>& bridgeEspNowChannel() { static std::atomic<uint8_t> channel{0}; return channel; }
|
||||
} }
|
||||
struct Backend : mesh::wireless::Backend {
|
||||
|
||||
Vendored
+11
@@ -1,6 +1,7 @@
|
||||
// Only peripheral endpoints are doubles. The readiness helper and startup
|
||||
// method are extracted verbatim from production WebConfigServer.cpp.
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -368,6 +369,16 @@ void reset(Fault fault = Fault::None) {
|
||||
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,
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ struct FakeWiFi {
|
||||
bool next_begin_connects = false;
|
||||
std::string ssid, password;
|
||||
int status() const { return state; }
|
||||
void mode(int) {}
|
||||
bool mode(int) { return true; }
|
||||
void disconnect(bool, bool) { ++disconnects; state = WL_DISCONNECTED; }
|
||||
void softAPdisconnect(bool) {}
|
||||
IP localIP() const { return {}; }
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// Model the pinned ESP-IDF 4.4.7 shared LR flag, not independent STA/AP masks.
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <helpers/WirelessControl.h>
|
||||
#include <helpers/ESPNowRawFragmentation.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
#define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c))
|
||||
#define ESPNOW_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define portENTER_CRITICAL(x) ((void)0)
|
||||
#define portEXIT_CRITICAL(x) ((void)0)
|
||||
#define portMUX_INITIALIZER_UNLOCKED 0
|
||||
#define ESP_NOW_ETH_ALEN 6
|
||||
#define WIFI_INIT_CONFIG_DEFAULT() 0
|
||||
using portMUX_TYPE=int;
|
||||
using esp_err_t=int;
|
||||
using wifi_interface_t=int;
|
||||
using wifi_second_chan_t=int;
|
||||
using wifi_init_config_t=int;
|
||||
enum wifi_mode_t {WIFI_OFF=0,WIFI_STA=1,WIFI_AP=2,WIFI_AP_STA=3};
|
||||
constexpr wifi_mode_t WIFI_MODE_NULL=WIFI_OFF,WIFI_MODE_STA=WIFI_STA,WIFI_MODE_AP=WIFI_AP;
|
||||
constexpr int ESP_OK=0,ESP_ERR_INVALID_STATE=-2,ESP_ERR_WIFI_NOT_INIT=-3;
|
||||
constexpr int WIFI_IF_STA=0,WIFI_IF_AP=1,WIFI_STORAGE_RAM=0,WIFI_SECOND_CHAN_NONE=0;
|
||||
constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4,WIFI_PROTOCOL_LR=8;
|
||||
constexpr int WIFI_PHY_MODE_LR=1,WIFI_PHY_RATE_LORA_250K=2,WIFI_SCAN_RUNNING=-1;
|
||||
enum wl_status_t {WL_IDLE_STATUS,WL_CONNECTED,WL_NO_SSID_AVAIL,WL_CONNECT_FAILED,WL_DISCONNECTED};
|
||||
enum esp_now_send_status_t {ESP_NOW_SEND_SUCCESS=0,ESP_NOW_SEND_FAIL=1};
|
||||
struct esp_now_send_info_t {};
|
||||
struct esp_now_recv_info_t {const uint8_t* src_addr;};
|
||||
struct esp_now_peer_info_t {uint8_t peer_addr[6];int channel,ifidx;bool encrypt;};
|
||||
struct esp_now_rate_config_t {int phymode,rate;bool ersu,dcm;};
|
||||
namespace mesh {enum class RadioParamApplyResult {APPLIED};}
|
||||
int failure=0,mode_reads=0,station_writes=0,ap_writes=0,storage_writes=0;
|
||||
int init_calls=0,deinit_calls=0,wake_refs=0,send_calls=0,power=0,rate_calls=0;
|
||||
bool driver_initialized=true,sdk_active=false,storage_ram=false,lr=false,nvs_lr=false;
|
||||
bool ap_during_station_write=false;
|
||||
wifi_mode_t sdk_mode=WIFI_STA;
|
||||
uint8_t sta_phy=7,ap_phy=7,current_channel=1;
|
||||
std::vector<int> protocol_order;
|
||||
uint32_t millis(){return 1234;}
|
||||
int esp_wifi_get_mode(wifi_mode_t* mode){
|
||||
++mode_reads;if(failure==1 || (failure==2&&mode_reads==2))return -1;
|
||||
if(!driver_initialized)return ESP_ERR_WIFI_NOT_INIT;
|
||||
*mode=sdk_mode;return ESP_OK;
|
||||
}
|
||||
int esp_wifi_set_storage(int){++storage_writes;if(failure==3)return -1;storage_ram=true;return ESP_OK;}
|
||||
int esp_wifi_set_protocol(int interface_id,uint8_t mask){
|
||||
if((interface_id==WIFI_IF_STA&&failure==4)||(interface_id==WIFI_IF_AP&&failure==5))return -1;
|
||||
if(interface_id==WIFI_IF_STA){++station_writes;sta_phy=mask&7;}else{++ap_writes;ap_phy=mask&7;}
|
||||
lr=(mask&WIFI_PROTOCOL_LR)!=0;
|
||||
if(!storage_ram)nvs_lr=lr;
|
||||
protocol_order.push_back(interface_id);
|
||||
if(interface_id==WIFI_IF_STA&&ap_during_station_write){sdk_mode=WIFI_AP_STA;ap_during_station_write=false;}
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_get_protocol(int interface_id,uint8_t* mask){
|
||||
if(failure==6)return -1;
|
||||
*mask=(interface_id==WIFI_IF_STA?sta_phy:ap_phy)|(lr?8:0);
|
||||
if(failure==7)*mask|=8;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_get_channel(uint8_t* c,wifi_second_chan_t*){*c=current_channel;return ESP_OK;}
|
||||
int esp_wifi_set_channel(uint8_t c,int){current_channel=c;return ESP_OK;}
|
||||
int esp_wifi_init(wifi_init_config_t*){driver_initialized=true;return ESP_OK;}
|
||||
int esp_wifi_set_mode(wifi_mode_t mode){sdk_mode=mode;return ESP_OK;}
|
||||
int esp_wifi_start(){return ESP_OK;}
|
||||
int esp_wifi_stop(){return ESP_OK;}
|
||||
int esp_wifi_deinit(){driver_initialized=false;return ESP_OK;}
|
||||
struct FakeWiFi {
|
||||
bool facade_started=true,autoreconnect=true;
|
||||
wl_status_t state=WL_DISCONNECTED;int join_channel=0,scan_channel=0;
|
||||
void persistent(bool){} // A late Arduino preference does not switch SDK storage.
|
||||
wifi_mode_t getMode(){return facade_started?sdk_mode:WIFI_OFF;}
|
||||
int channel(int index=-1){return index<0?current_channel:1;}
|
||||
void setAutoReconnect(bool value){autoreconnect=value;}
|
||||
bool mode(wifi_mode_t value){if(failure==8)return false;driver_initialized=true;facade_started=true;sdk_mode=value;return true;}
|
||||
bool softAPdisconnect(bool){sdk_mode=WIFI_STA;return true;}
|
||||
struct IP {struct Text {const char* c_str(){return "192.0.2.1";}};Text toString(){return {};}};
|
||||
IP localIP(){return {};}
|
||||
wl_status_t status(){return state;}
|
||||
wl_status_t begin(const char*,const char*,int channel=0,const uint8_t* =nullptr){join_channel=channel;state=WL_CONNECTED;return state;}
|
||||
int scanComplete(){return -2;}void scanDelete(){}
|
||||
int scanNetworks(bool,bool,bool,int,int channel,const char*,const uint8_t*){scan_channel=channel;return 1;}
|
||||
std::string SSID(int){return "mesh";}int RSSI(int){return -50;}
|
||||
const uint8_t* BSSID(int){static uint8_t mac[6]={1};return mac;}
|
||||
void disconnect(bool,bool){state=WL_DISCONNECTED;}
|
||||
} WiFi;
|
||||
int esp_now_init(){++init_calls;sdk_active=true;return ESP_OK;}
|
||||
int esp_now_deinit(){assert(sdk_active);sdk_active=false;++deinit_calls;return ESP_OK;}
|
||||
int esp_wifi_force_wakeup_acquire(){++wake_refs;return ESP_OK;}
|
||||
int esp_wifi_force_wakeup_release(){assert(wake_refs==1);--wake_refs;return ESP_OK;}
|
||||
int esp_wifi_set_max_tx_power(int value){power=value;return ESP_OK;}
|
||||
template<typename T> int esp_now_register_send_cb(T){return ESP_OK;}
|
||||
template<typename T> int esp_now_register_recv_cb(T){return ESP_OK;}
|
||||
int esp_now_unregister_send_cb(){return ESP_OK;}
|
||||
int esp_now_unregister_recv_cb(){return ESP_OK;}
|
||||
int esp_now_add_peer(esp_now_peer_info_t*){return ESP_OK;}
|
||||
int esp_now_del_peer(const uint8_t*){return ESP_OK;}
|
||||
int esp_now_set_peer_rate_config(const uint8_t*,esp_now_rate_config_t*){++rate_calls;return ESP_OK;}
|
||||
int esp_wifi_config_espnow_rate(int,int){++rate_calls;return ESP_OK;}
|
||||
int esp_now_send(const uint8_t*,const uint8_t*,size_t){assert(sdk_active);++send_calls;return ESP_OK;}
|
||||
void esp_efuse_mac_get_default(uint8_t* mac){memset(mac,1,6);}
|
||||
class Preferences {
|
||||
public:
|
||||
bool begin(const char*,bool){return true;}bool isKey(const char*){return false;}
|
||||
uint8_t getUChar(const char*,uint8_t fallback){return fallback;}
|
||||
size_t putUChar(const char*,uint8_t){return 1;}void end(){}
|
||||
};
|
||||
struct Backend : mesh::wireless::Backend {
|
||||
bool wifi=true;
|
||||
uint8_t available()const override{return mesh::wireless::WiFi|mesh::wireless::EspNow;}
|
||||
uint8_t enabled()const override{return wifi?mesh::wireless::WiFi:0;}
|
||||
uint8_t clients()const override{return mesh::wireless::Independent;}
|
||||
mesh::wireless::Result set(uint8_t,bool)override{return mesh::wireless::Result::Done;}
|
||||
};
|
||||
Vendored
+10
@@ -1,6 +1,7 @@
|
||||
// WiFi and the HTTP server are peripheral fakes; both OTA lifecycle methods
|
||||
// are inserted verbatim from ESP32Board.cpp by test_wifi_ota_start.py.
|
||||
#include <cassert>
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -222,6 +223,15 @@ const esp_partition_t* esp_ota_get_next_update_partition(void*) {
|
||||
int main() {
|
||||
ESP32Board board;
|
||||
struct { char text[160] = {}; uint32_t guard = 0x12345678; } reply;
|
||||
for (uint8_t owner : {mesh::wifi::kLongRangeRadioOwner, mesh::wifi::kLongRangeBridgeOwner}) {
|
||||
mesh::wifi::setLongRangeOwner(owner, true);
|
||||
assert(!board.startOTAUpdate("test node", reply.text, true));
|
||||
assert(strstr(reply.text, "stop ESP-NOW") && WiFi.starts == 0 && !WiFi.ap);
|
||||
assert(!board.inhibit_sleep && !board.ota_server);
|
||||
assert(!mesh::wifi::startOpenAccessPoint("MeshCore-OTA", false));
|
||||
assert(WiFi.starts == 0 && !WiFi.ap);
|
||||
mesh::wifi::setLongRangeOwner(owner, false);
|
||||
}
|
||||
auto start = [&](bool ap, const char* ip, const char* instruction) {
|
||||
assert(board.startOTAUpdate("test node", reply.text, ap));
|
||||
assert(board.inhibit_sleep && board.ota_server);
|
||||
|
||||
@@ -41,6 +41,10 @@ struct FakeWiFi {
|
||||
namespace mesh {
|
||||
enum class RadioParamApplyResult {APPLIED};
|
||||
namespace wifi {
|
||||
constexpr uint8_t kProtocolMask=15,kLongRangeRadioOwner=1,kLongRangeBridgeOwner=2;
|
||||
void setLongRangeOwner(uint8_t,bool){}
|
||||
int checkLongRangeRadioStart(){return WiFi.current&WIFI_AP?-1:0;}
|
||||
int applyStationProtocolMask(uint8_t,bool){return failure==2?-1:0;}
|
||||
int applyProtocolMask(int){return failure==2?-1:0;}
|
||||
int restoreEspNowChannel(){return failure==3?-1:0;}
|
||||
}
|
||||
@@ -94,9 +98,11 @@ int main(){
|
||||
OnDataSent(nullptr,ESP_NOW_SEND_SUCCESS); // callback arriving while disabled
|
||||
deliver(raw,4);assert(radio.recvRaw(out,sizeof(out))==0);
|
||||
radio.end();assert(wake_refs==0&&!sdk_active);
|
||||
// Starting alongside a setup AP preserves it and restores the configured TX power.
|
||||
// LR cannot restart alongside a conventional setup AP on the shared PHY.
|
||||
WiFi.current=WIFI_AP_STA;WiFi.autoreconnect=true;radio.init();
|
||||
assert(WiFi.current==WIFI_AP_STA&&WiFi.autoreconnect&&power==52);
|
||||
assert(!radio.isEnabled()&&!sdk_active&&WiFi.current==WIFI_AP_STA);
|
||||
WiFi.current=WIFI_STA;radio.init();
|
||||
assert(WiFi.current==WIFI_STA&&WiFi.autoreconnect&&power==52);
|
||||
assert(!radio.isSendComplete()); // restarting before Dispatcher's timeout cannot fake TX success
|
||||
radio.onSendFinished(); // Dispatcher acknowledges the cancelled operation
|
||||
assert(radio.recvRaw(out,sizeof(out))==0&&radio.isInRecvMode());
|
||||
@@ -250,16 +256,20 @@ int main(){
|
||||
def test_bridge_channel_constraint_tracks_runtime_state(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
folder=Path(tmp)
|
||||
(folder/'esp_err.h').write_text('#pragma once\nusing esp_err_t=int;\nconstexpr int ESP_OK=0;\n')
|
||||
(folder/'esp_err.h').write_text('#pragma once\nusing esp_err_t=int;\nconstexpr int ESP_OK=0,ESP_ERR_INVALID_STATE=-2,ESP_ERR_WIFI_NOT_INIT=-3;\n')
|
||||
(folder/'esp_wifi.h').write_text(r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <esp_err.h>
|
||||
using wifi_interface_t=int;using wifi_second_chan_t=int;
|
||||
using wifi_interface_t=int;using wifi_second_chan_t=int;using wifi_mode_t=int;
|
||||
constexpr int WIFI_MODE_NULL=0,WIFI_MODE_STA=1,WIFI_MODE_AP=2,WIFI_STORAGE_RAM=0;
|
||||
constexpr int WIFI_IF_STA=0,WIFI_IF_AP=1,WIFI_SECOND_CHAN_NONE=0;
|
||||
constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4,WIFI_PROTOCOL_LR=8;
|
||||
uint8_t current_channel=1,last_protocol=0;
|
||||
int esp_wifi_set_protocol(int,uint8_t p){last_protocol=p;return 0;}
|
||||
int esp_wifi_get_mode(int* p){*p=WIFI_MODE_STA;return 0;}
|
||||
int esp_wifi_get_protocol(int,uint8_t* p){*p=7;return 0;}
|
||||
int esp_wifi_set_storage(int){return 0;}
|
||||
int esp_wifi_get_channel(uint8_t* p,int*){*p=current_channel;return 0;}
|
||||
int esp_wifi_set_channel(uint8_t p,int){current_channel=p;return 0;}
|
||||
''')
|
||||
|
||||
@@ -34,6 +34,9 @@ class WebConfigApStartTest(unittest.TestCase):
|
||||
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]
|
||||
owner_policy = policy[policy.index("static constexpr uint8_t kLongRangeRadioOwner ="):]
|
||||
owner_policy = owner_policy[:owner_policy.index("// A bridge can start/stop")]
|
||||
mask += "\n" + owner_policy
|
||||
fixture = FIXTURE.read_text(encoding="utf-8").replace("@AP_MASK@", mask)
|
||||
ap_policy = (ROOT / "src/helpers/esp32/WiFiAccessPointPolicy.h").read_text(encoding="utf-8")
|
||||
scanner = "namespace mesh { namespace wifi {\n" + "\n".join(
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Execute production policies and ESP-NOW starts with IDF 4.4 shared LR state."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from test_radio_receive_contract import method
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BRIDGE_CLASS = r"""
|
||||
class Bridge {
|
||||
public:
|
||||
struct Prefs {int bridge_format=mesh::bridge::ESPNOW_FORMAT_RAW;uint8_t bridge_channel=1;} prefs;
|
||||
Prefs* _prefs=&prefs;bool _initialized=false;
|
||||
int _active_format=0,_rx_mux=0,_rx_head=0,_rx_tail=0,_rx_count=0,_rx_dropped=0,_rx_dropped_reported=0;
|
||||
int _tx_mux=0,_tx_head=0,_tx_tail=0,_tx_count=0,_tx_waiting=0,_tx_callback_done=0;
|
||||
int _tx_callback_status=0,_tx_dropped=0,_tx_dropped_reported=0;
|
||||
mesh::espnow::ESPNowRawReassembler _raw_reassembler;
|
||||
struct QueuedTransmit {bool started=false;int packet=0;};
|
||||
static constexpr int TX_QUEUE_DEPTH=2;
|
||||
QueuedTransmit _tx_queue[TX_QUEUE_DEPTH];
|
||||
struct {void clear(const int*){}} _seen_packets;
|
||||
static void recv_cb(){}static void send_cb(){}
|
||||
void begin();void end();
|
||||
};
|
||||
"""
|
||||
WEB_CLASS = r"""
|
||||
namespace mesh {
|
||||
struct DebugPort {template<class... T> void printf(const char*,T...){}void println(const char*){}};
|
||||
DebugPort& usbDebugPort(){static DebugPort port;return port;}
|
||||
}
|
||||
class WebTeardown {
|
||||
public:
|
||||
enum Mode {MODE_SETUP,MODE_LAN};
|
||||
struct DNS {void stop(){}};
|
||||
DNS* _dns=new DNS;
|
||||
Mode _mode=MODE_SETUP;
|
||||
bool _was_setup_ap=true,_initial_setup=true,_retry_saved_wifi_in_setup=true,_setup_reconnect_in_progress=true;
|
||||
uint32_t _setup_started_at=1,_setup_reconnect_deadline=1,_last_activity=1;
|
||||
char _wifi_ssid[5]="mesh";
|
||||
struct Tracker {void noteConnected(){}} _wifi_reconnect_tracker;
|
||||
~WebTeardown(){delete _dns;}
|
||||
void promote(){uint32_t now=1234;@PROMOTE@}
|
||||
void idle(){uint32_t now=1234;@IDLE@}
|
||||
};
|
||||
"""
|
||||
CHECKS = r"""
|
||||
void reset_policy(){
|
||||
failure=0;mode_reads=station_writes=ap_writes=storage_writes=0;
|
||||
driver_initialized=true;storage_ram=false;lr=false;nvs_lr=false;
|
||||
sdk_mode=WIFI_STA;sta_phy=ap_phy=7;ap_during_station_write=false;
|
||||
WiFi.facade_started=true;protocol_order.clear();
|
||||
}
|
||||
void check_ap(){uint8_t actual=0;assert(esp_wifi_get_protocol(WIFI_IF_AP,&actual)==ESP_OK&&actual==7);}
|
||||
int main(int argc,char** argv){
|
||||
assert(argc==2);std::string test=argv[1];Backend backend;mesh::wireless::control().begin(backend);
|
||||
if(test=="station"){
|
||||
sdk_mode=WIFI_AP_STA;WiFi.facade_started=false;
|
||||
// Witness the SDK coupling: a direct STA15 write changes AP's live bitmap.
|
||||
assert(esp_wifi_set_protocol(WIFI_IF_STA,15)==ESP_OK);
|
||||
uint8_t actual=0;esp_wifi_get_protocol(WIFI_IF_AP,&actual);assert(actual==15);
|
||||
// Production policy repairs it even if the Arduino facade reports OFF.
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)==ESP_OK);check_ap();
|
||||
assert(!lr&&station_writes==2&&ap_writes==1);
|
||||
assert(mesh::wifi::beginStation("mesh","password")==WL_CONNECTED);
|
||||
assert(WiFi.scan_channel==1&&WiFi.join_channel==1);check_ap();
|
||||
// Same unchanged helper is used by setup saved-SSID retry and credential handoff.
|
||||
assert(mesh::wifi::beginStation("mesh","password")==WL_CONNECTED);check_ap();
|
||||
sdk_mode=WIFI_STA;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)==ESP_OK&&lr&&storage_ram);
|
||||
}else if(test=="bridge"){
|
||||
Bridge bridge;sdk_mode=WIFI_AP_STA;WiFi.facade_started=false;
|
||||
assert(mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
bridge.begin();assert(!bridge._initialized&&!sdk_active&&init_calls==0);
|
||||
assert(sdk_mode==WIFI_AP_STA&&station_writes==0&&storage_writes==0);check_ap();
|
||||
// Saved raw intent is unchanged; the normal retry can start after AP stops.
|
||||
sdk_mode=WIFI_STA;bridge.begin();
|
||||
assert(bridge._initialized&&sdk_active&&lr&&storage_ram&&!nvs_lr&&rate_calls==1);
|
||||
assert(!mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
bridge.begin();assert(mesh::wifi::longRangeOwners().load()==mesh::wifi::kLongRangeBridgeOwner);
|
||||
failure=2;mode_reads=0;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)!=ESP_OK&&lr);
|
||||
failure=0;mode_reads=0;
|
||||
assert(mesh::wifi::bridgeEspNowChannel().load()==1);
|
||||
bridge.end();assert(!sdk_active&&mesh::wifi::bridgeEspNowChannel().load()==0);
|
||||
bridge.end();assert(mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
sdk_mode=WIFI_AP_STA;mesh::wifi::applyAccessPointProtocolMask();
|
||||
bridge.begin();assert(!bridge._initialized);check_ap();
|
||||
bridge.prefs.bridge_format=mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
||||
bridge.begin();assert(bridge._initialized&&mesh::wifi::accessPointCompatibleWithLongRange());check_ap();
|
||||
bridge.end();assert(mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
}else if(test=="radio"){
|
||||
ESPNOWRadio radio;sdk_mode=WIFI_AP_STA;WiFi.facade_started=false;
|
||||
radio.init();assert(!radio.isEnabled()&&!sdk_active&&init_calls==0);
|
||||
assert(sdk_mode==WIFI_AP_STA&&station_writes==0&&storage_writes==0);check_ap();
|
||||
sdk_mode=WIFI_STA;radio.init();
|
||||
assert(radio.isEnabled()&&sdk_active&&lr&&storage_ram&&!nvs_lr);
|
||||
assert(!mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
radio.init();assert(mesh::wifi::longRangeOwners().load()==mesh::wifi::kLongRangeRadioOwner);
|
||||
#if defined(MESH_ESPNOW_RADIO) && MESH_ESPNOW_RADIO
|
||||
assert(rate_calls==1);
|
||||
#endif
|
||||
radio.end();assert(!sdk_active&&wake_refs==0);
|
||||
radio.end();assert(mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
sdk_mode=WIFI_AP_STA;mesh::wifi::applyAccessPointProtocolMask();
|
||||
radio.init();assert(!radio.isEnabled());check_ap();
|
||||
}else if(test=="failures"){
|
||||
for(int stage:{1,2,4,5,6,7}){
|
||||
reset_policy();sdk_mode=WIFI_AP_STA;failure=stage;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)!=ESP_OK);
|
||||
assert(!lr); // no failure path writes LR into the live AP
|
||||
}
|
||||
reset_policy();failure=3;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)!=ESP_OK);
|
||||
assert(station_writes==0&&!lr&&!nvs_lr);
|
||||
reset_policy();driver_initialized=false;
|
||||
assert(mesh::wifi::checkLongRangeRadioStart()==ESP_OK);
|
||||
reset_policy();failure=1;
|
||||
assert(mesh::wifi::checkLongRangeRadioStart()!=ESP_OK);
|
||||
for(int stage:{1,3,4}){
|
||||
reset_policy();failure=stage;Bridge bridge;bridge.begin();
|
||||
assert(!bridge._initialized&&!sdk_active&&!nvs_lr);
|
||||
assert(mesh::wifi::accessPointCompatibleWithLongRange());
|
||||
}
|
||||
}else if(test=="race"){
|
||||
// A second owner enables AP during the STA write. Post-write repair must
|
||||
// preserve standard beacons and reject a claimed strict-LR success.
|
||||
ap_during_station_write=true;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)==ESP_OK);check_ap();
|
||||
reset_policy();ap_during_station_write=true;
|
||||
assert(mesh::wifi::applyStationProtocolMask(15,true)!=ESP_OK);check_ap();
|
||||
assert(!nvs_lr);
|
||||
for(int stage:{2,5}){
|
||||
reset_policy();ap_during_station_write=true;failure=stage;
|
||||
assert(mesh::wifi::applyStationProtocolMask(15,true)!=ESP_OK);
|
||||
assert(!lr&&!nvs_lr); // uncertain/failed AP repair cannot leave a new LR flag
|
||||
}
|
||||
}else if(test=="stop"){
|
||||
sdk_mode=WIFI_AP_STA;
|
||||
assert(mesh::wifi::applyProtocolMask(WIFI_IF_STA)==ESP_OK);check_ap();
|
||||
mesh::wifi::stopTemporaryAccessPointRadio(false);check_ap();assert(!lr);
|
||||
sdk_mode=WIFI_STA;
|
||||
mesh::wifi::stopTemporaryAccessPointRadio(false);
|
||||
assert(lr&&storage_ram&&!nvs_lr);
|
||||
}else if(test=="web_teardown"){
|
||||
for(bool idle:{false,true}){
|
||||
for(int stage:{3,8}){
|
||||
reset_policy();sdk_mode=WIFI_AP_STA;WiFi.state=WL_CONNECTED;failure=stage;
|
||||
WebTeardown web;
|
||||
if(idle)web.idle();else web.promote();
|
||||
assert(web._mode==WebTeardown::MODE_SETUP&&web._was_setup_ap);
|
||||
// A failed restore keeps setup ownership flags so a later tick can retry.
|
||||
failure=0;
|
||||
if(idle)web.idle();else web.promote();
|
||||
assert(web._mode==WebTeardown::MODE_LAN&&!web._was_setup_ap&&lr&&storage_ram);
|
||||
}
|
||||
}
|
||||
}else return 2;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def build_and_run(scenario, *, mutation=None, full=True):
|
||||
policy = (ROOT / 'src/helpers/esp32/WiFiRadioPolicy.h').read_text()
|
||||
radio_header = (ROOT / 'src/helpers/esp32/ESPNOWRadio.h').read_text()
|
||||
radio_header = radio_header[:radio_header.index('#if ESPNOW_DEBUG_LOGGING')]
|
||||
radio_header = re.sub(r'^\s*#(?:include|pragma).*$', '', radio_header, flags=re.MULTILINE)
|
||||
radio_header = radio_header.replace(' : public mesh::Radio', '').replace(' override', '')
|
||||
radio = (ROOT / 'src/helpers/esp32/ESPNOWRadio.cpp').read_text()
|
||||
radio = re.sub(r'^\s*#include.*$', '', radio, flags=re.MULTILINE)
|
||||
bridge_source = (ROOT / 'src/helpers/bridges/ESPNowBridge.cpp').read_text()
|
||||
bridge = method(bridge_source, 'static void stopBridgeWiFiIfUnused(') + '\n'
|
||||
bridge += '\n'.join(method(bridge_source, f'void ESPNowBridge::{name}(')
|
||||
.replace('ESPNowBridge::', 'Bridge::') for name in ('begin', 'end'))
|
||||
stop = method((ROOT / 'src/helpers/esp32/WiFiAccessPointPolicy.h').read_text(),
|
||||
'inline void stopTemporaryAccessPointRadio(')
|
||||
web_source = (ROOT / 'src/helpers/esp32/WebConfigServer.cpp').read_text()
|
||||
tick = method(web_source, 'void WebConfigServer::tick(')
|
||||
setup = method(tick, 'if (_mode == MODE_SETUP && _retry_saved_wifi_in_setup && _wifi_ssid[0])')
|
||||
promotion = method(setup, 'if (WiFi.status() == WL_CONNECTED)')
|
||||
idle_outer = method(tick, 'if (_mode == MODE_SETUP && WiFi.softAPgetStationNum() == 0 &&')
|
||||
idle = method(idle_outer, 'if (_retry_saved_wifi_in_setup && _wifi_ssid[0])')
|
||||
if mutation == 'unguarded_station':
|
||||
policy = policy.replace('if (mode & WIFI_MODE_AP) protocols = kAccessPointProtocolMask;', '')
|
||||
policy = policy.replace('if (mode & WIFI_MODE_AP) {\n result = applyAccessPointProtocolMask();',
|
||||
'if (false) {\n result = applyAccessPointProtocolMask();')
|
||||
elif mutation == 'raw_start':
|
||||
bridge = bridge.replace('raw_format && mesh::wifi::checkLongRangeRadioStart() != ESP_OK', 'false')
|
||||
policy = policy.replace('if ((mode & WIFI_MODE_AP) && require_lr)', 'if (false)')
|
||||
policy = policy.replace('if (mode & WIFI_MODE_AP) protocols = kAccessPointProtocolMask;', '')
|
||||
policy = policy.replace('if (mode & WIFI_MODE_AP) {\n result = applyAccessPointProtocolMask();',
|
||||
'if (false) {\n result = applyAccessPointProtocolMask();')
|
||||
elif mutation == 'primary_start':
|
||||
radio = radio.replace('mesh::wifi::checkLongRangeRadioStart() != ESP_OK', 'false')
|
||||
elif mutation == 'flash_storage':
|
||||
policy = policy.replace('result = esp_wifi_set_storage(WIFI_STORAGE_RAM);', 'result = ESP_OK;')
|
||||
elif mutation == 'no_restore':
|
||||
stop = stop.replace('applyProtocolMask(WIFI_IF_STA);', '')
|
||||
elif mutation == 'web_no_restore':
|
||||
promotion = promotion.replace('|| mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK', '')
|
||||
idle = idle.replace('|| mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK', '')
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
folder = Path(temporary)
|
||||
(folder / 'helpers/esp32').mkdir(parents=True)
|
||||
(folder / 'helpers/esp32/WiFiRadioPolicy.h').write_text(policy)
|
||||
for name in ('esp_err.h', 'esp_wifi.h', 'WiFi.h', 'Preferences.h'):
|
||||
(folder / name).write_text('#pragma once\n')
|
||||
fixture = (ROOT / 'test/fixtures/wifi_ap_shared_lr_sdk.h').read_text()
|
||||
source = fixture + '\n#include <helpers/esp32/WiFiRadioPolicy.h>\n'
|
||||
source += '#include <helpers/esp32/WiFiStationPolicy.h>\n'
|
||||
source += 'namespace mesh {namespace wifi {\n' + stop + '\n}}\n'
|
||||
web = WEB_CLASS.replace('@PROMOTE@', promotion).replace('@IDLE@', idle)
|
||||
source += radio_header + radio + BRIDGE_CLASS + bridge + web + CHECKS
|
||||
(folder / 'test.cpp').write_text(source)
|
||||
binary = folder / 'test'
|
||||
flags = ['-DMESH_PRIMARY_ESPNOW=1', '-DESP_ARDUINO_VERSION_MAJOR=2', '-DESP_IDF_VERSION=40407']
|
||||
if full:
|
||||
flags += ['-DMESH_ESPNOW_RADIO=1', '-DWITH_ESPNOW_BRIDGE=1']
|
||||
compiled = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
'-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie',
|
||||
'-I', str(folder), '-I', str(ROOT/'src'), *flags, str(folder/'test.cpp'), '-o', str(binary)],
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if compiled.returncode:
|
||||
raise AssertionError(compiled.stderr)
|
||||
return subprocess.run([str(binary), scenario], text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
class WifiApLongRangeSafetyTests(unittest.TestCase):
|
||||
def assert_pass(self, scenario, **kwargs):
|
||||
result = build_and_run(scenario, **kwargs)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_constrained_setup_retry_and_handoff_preserve_ap(self):
|
||||
self.assert_pass('station')
|
||||
source = (ROOT/'src/helpers/esp32/WebConfigServer.cpp').read_text()
|
||||
tick = method(source, 'void WebConfigServer::tick(')
|
||||
batch = method(source, 'void WebConfigServer::drainBatch(')
|
||||
self.assertIn('mesh::wifi::beginStation(_wifi_ssid, _wifi_password);', tick)
|
||||
self.assertIn('mesh::wifi::beginStation(_wifi_ssid, _wifi_password);', batch)
|
||||
|
||||
def test_raw_bridge_defers_then_resumes_without_persisting_lr(self):
|
||||
self.assert_pass('bridge')
|
||||
|
||||
def test_primary_restart_defers_for_sdk_ap_even_with_stale_facade(self):
|
||||
self.assert_pass('radio')
|
||||
self.assert_pass('radio', full=False)
|
||||
|
||||
def test_protocol_read_write_and_storage_failures_do_not_claim_success(self):
|
||||
self.assert_pass('failures')
|
||||
|
||||
def test_ap_start_during_station_write_is_repaired(self):
|
||||
self.assert_pass('race')
|
||||
|
||||
def test_lr_restore_waits_for_actual_ap_stop(self):
|
||||
self.assert_pass('stop')
|
||||
|
||||
def test_webconfig_teardown_failure_retains_state_for_retry(self):
|
||||
self.assert_pass('web_teardown')
|
||||
|
||||
def test_negative_controls_fail_at_runtime(self):
|
||||
for mutation, scenario in [('unguarded_station', 'station'), ('raw_start', 'bridge'),
|
||||
('primary_start', 'radio'), ('flash_storage', 'bridge'),
|
||||
('no_restore', 'stop'), ('web_no_restore', 'web_teardown')]:
|
||||
with self.subTest(mutation=mutation):
|
||||
result = build_and_run(scenario, mutation=mutation)
|
||||
self.assertNotEqual(result.returncode, 0, 'regression was not detected')
|
||||
self.assertIn('Assertion', result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -123,6 +123,7 @@ struct MockWiFi {
|
||||
void scanDelete() {}
|
||||
} WiFi;
|
||||
namespace mesh { namespace wifi {
|
||||
bool accessPointCompatibleWithLongRange() { return true; }
|
||||
constexpr uint8_t kAccessPointProtocolMask = 7;
|
||||
int applyAccessPointProtocolMask() { WiFi.ap_protocol = 7; return ESP_OK; }
|
||||
int applyProtocolMask(int) { return ESP_OK; }
|
||||
|
||||
@@ -36,7 +36,9 @@ class WiFiOtaStartTest(unittest.TestCase):
|
||||
policy = (ROOT / "src/helpers/esp32/WiFiRadioPolicy.h").read_text()
|
||||
mask = re.search(r"static constexpr uint8_t kAccessPointProtocolMask =.*?;",
|
||||
policy, re.DOTALL).group(0)
|
||||
fixture = fixture.replace("@AP_PROTOCOL_POLICY@", mask + "\n" + extract_braced(
|
||||
owner_policy = policy[policy.index("static constexpr uint8_t kLongRangeRadioOwner ="):]
|
||||
owner_policy = owner_policy[:owner_policy.index("// A bridge can start/stop")]
|
||||
fixture = fixture.replace("@AP_PROTOCOL_POLICY@", mask + "\n" + owner_policy + extract_braced(
|
||||
policy, "inline esp_err_t applyAccessPointProtocolMask()") + "\n"
|
||||
+ extract_braced(policy, "inline int accessPointChannel()"))
|
||||
lifecycle = (ROOT / "src/helpers/esp32/WiFiAccessPointPolicy.h").read_text()
|
||||
|
||||
Reference in new issue
Block a user