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:
mikecarper committed 2026-10-07 02:07:06 -07:00
1 parent 526af2a82b
commit 8c7d7d8839
17 files changed
+557 -17

No files matched your search

+1
View File
@@ -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
+10
View File
@@ -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) {
+8 -3
View File
@@ -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);
+10 -2
View File
@@ -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();
+8 -2
View File
@@ -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()
+74 -4
View File
@@ -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
View File
@@ -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 {
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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;}
};
+10
View File
@@ -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);
+14 -4
View File
@@ -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;}
''')
+3
View File
@@ -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(
+273
View File
@@ -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()
+1
View File
@@ -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; }
+3 -1
View File
@@ -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()