Resume UDP station ownership after WebConfig fallback
This commit is contained in:
1 parent
9b80ee63ee
commit
36d9735e55
9 files changed
+247
-34
No files matched your search
@@ -20,6 +20,14 @@ WiFi ownership transitions, then resumes when station operation is available.
|
||||
Opening a captive configuration access point does not create a UDP bridge
|
||||
network. USB/LoRa CLI access remains useful for inspecting a paused bridge.
|
||||
|
||||
With saved WiFi credentials, enabling UDP requests a connection to that
|
||||
network. The web page can share the same station connection. An unused
|
||||
automatic recovery AP returns to station mode when UDP needs the network;
|
||||
connected setup clients and unfinished browser/OTA work keep their link.
|
||||
An explicitly requested `start webconfig ap` stays active until released.
|
||||
Use `stop webconfig` when finished with that AP, then check `get udp.status`.
|
||||
Switching WiFi back on does not enable UDP if you saved `udp.enabled off`.
|
||||
|
||||
## Router client isolation
|
||||
|
||||
Client isolation is an optional access-point or guest-network policy, not a
|
||||
|
||||
@@ -11486,6 +11486,17 @@ bool MyMesh::startWebConfig(bool force_ap, char* reply) {
|
||||
return startWebConfigImpl(force_ap, reply, false);
|
||||
}
|
||||
|
||||
bool MyMesh::webConfigOwnsWiFi() const {
|
||||
bool owns_wifi = true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
owns_wifi = !mqtt_bridge || (!mqtt_bridge->isRunning() && !mqtt_bridge->isStopping());
|
||||
#endif
|
||||
#ifdef WITH_UDP_BRIDGE
|
||||
owns_wifi = owns_wifi && !isUDPBridgeNetworkEnabled();
|
||||
#endif
|
||||
return owns_wifi;
|
||||
}
|
||||
|
||||
bool MyMesh::startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup) {
|
||||
if (_cli.getBoard()->isOTAUpdateRunning()) {
|
||||
strcpy(reply, "Err: OTA server is running - 'stop ota' first");
|
||||
@@ -11518,11 +11529,9 @@ bool MyMesh::startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup
|
||||
#endif
|
||||
if (!_webconfig) {
|
||||
void* mqtt_prefs = nullptr;
|
||||
bool owns_wifi = true;
|
||||
const bool owns_wifi = webConfigOwnsWiFi();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
mqtt_prefs = _cli.getObserverPrefs();
|
||||
owns_wifi = !mqtt_bridge
|
||||
|| (!mqtt_bridge->isRunning() && !mqtt_bridge->isStopping());
|
||||
#endif
|
||||
_webconfig = new WebConfigServer(this, mqtt_prefs, owns_wifi,
|
||||
self_id.pub_key, getFirmwareVer(), getBuildDate(), getRole(),
|
||||
@@ -11542,10 +11551,7 @@ bool MyMesh::startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup
|
||||
// WiFi start (including master-radio restoration) must retain explicit ESP-NOW.
|
||||
if (automatic_setup) suspendUnconfiguredSetupBridges();
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_webconfig->updateWiFiOwnership(!mqtt_bridge
|
||||
|| (!mqtt_bridge->isRunning() && !mqtt_bridge->isStopping()));
|
||||
#endif
|
||||
_webconfig->updateWiFiOwnership(webConfigOwnsWiFi());
|
||||
|
||||
#ifdef WITH_UDP_BRIDGE
|
||||
// The portal may switch to AP/setup mode synchronously. Close the station
|
||||
@@ -13624,10 +13630,7 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
|
||||
mesh::usbConsolePort().printf("%s\r\n", wc_reply);
|
||||
}
|
||||
if (_webconfig) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_webconfig->updateWiFiOwnership(!mqtt_bridge
|
||||
|| (!mqtt_bridge->isRunning() && !mqtt_bridge->isStopping()));
|
||||
#endif
|
||||
_webconfig->updateWiFiOwnership(webConfigOwnsWiFi());
|
||||
_webconfig->tick(millis());
|
||||
if (!_webconfig->isRunning() && !_webconfig->isStopping()) {
|
||||
delete _webconfig;
|
||||
@@ -14352,13 +14355,15 @@ void MyMesh::serviceUDPBridge() {
|
||||
}
|
||||
// An AP, setup portal, OTA uploader or MQTT worker owns its WiFi lifecycle.
|
||||
// UDP may share its connected station, but never reconnects underneath it.
|
||||
if (!mesh::wifi::stationMutationAllowed()) return;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (isWebConfigActive() || isWebConfigStopping()) return;
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (mqtt_bridge && (mqtt_bridge->isRunning() || mqtt_bridge->isStopping())) return;
|
||||
#endif
|
||||
#ifdef WITH_WEBCONFIG
|
||||
// A LAN/connecting listener follows this station owner. An explicit setup
|
||||
// AP, active recovery browser or draining request retains its own link.
|
||||
if (_webconfig && !_webconfig->prepareStationOwner()) return;
|
||||
#endif
|
||||
if (!mesh::wifi::stationMutationAllowed()) return;
|
||||
if (!millisHasNowPassed(udp_station_retry_at)) return;
|
||||
udp_station_retry_at = millis() + 15000U;
|
||||
mesh::wifi::Credentials credentials;
|
||||
|
||||
@@ -733,6 +733,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool _unconfigured_setup_espnow_suspended = false;
|
||||
void suspendUnconfiguredSetupBridges();
|
||||
bool startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup);
|
||||
bool webConfigOwnsWiFi() const;
|
||||
bool _wc_batch_active = false;
|
||||
bool _wc_restart_pending = false;
|
||||
uint8_t _wc_slot_restart_mask = 0;
|
||||
|
||||
@@ -1053,6 +1053,52 @@ bool WebConfigServer::startAutoMode(char reply[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebConfigServer::prepareStationOwner() {
|
||||
if (_stopping) return false;
|
||||
updateWiFiOwnership(false);
|
||||
if (_mode != MODE_SETUP) return true;
|
||||
// Only startAutoMode's saved-network fallback is relinquished. A manual
|
||||
// setup AP never sets this flag, even when saved credentials exist.
|
||||
if (!_retry_saved_wifi_in_setup || WiFi.softAPgetStationNum() != 0) return false;
|
||||
const auto busy = [this]() {
|
||||
WCLock lock(_mux);
|
||||
return handlerRefCount() != 0 || _batch_state == BATCH_PENDING
|
||||
|| _setup_wifi_handoff_pending || _reboot_at
|
||||
|| (_terminal && (_terminal->running || _terminal->queue.pending))
|
||||
|| (_room_mailbox && (_room_mailbox->state() == mesh::RoomWebMailbox::State::Pending
|
||||
|| _room_mailbox->state() == mesh::RoomWebMailbox::State::Running));
|
||||
};
|
||||
if (busy()) return false;
|
||||
// Prevent an accepted OTA upload from racing the AP-to-STA transition.
|
||||
mesh::esp32::managementPassword::ChangeGuard handoff;
|
||||
if (!handoff.acquired()) return false;
|
||||
detachRoutes();
|
||||
if (busy()) {
|
||||
attachRoutes();
|
||||
return false;
|
||||
}
|
||||
WiFi.softAPdisconnect(true);
|
||||
if ((WiFi.getMode() & WIFI_AP) || !WiFi.mode(WIFI_STA)
|
||||
|| mesh::wifi::applyProtocolMask(WIFI_IF_STA) != ESP_OK) {
|
||||
attachRoutes();
|
||||
return false;
|
||||
}
|
||||
if (_dns) {
|
||||
_dns->stop();
|
||||
delete _dns;
|
||||
_dns = NULL;
|
||||
}
|
||||
_was_setup_ap = false;
|
||||
_initial_setup = false;
|
||||
_setup_provisioning_privilege = false;
|
||||
_setup_started_at = 0;
|
||||
_retry_saved_wifi_in_setup = false;
|
||||
_mode = MODE_CONNECTING;
|
||||
attachRoutes();
|
||||
mesh::usbDebugPort().println("WebConfig: station owner resumed; waiting for saved WiFi");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebConfigServer::createServer() {
|
||||
if (_host == NULL) {
|
||||
_host = new AsyncWebServer(80);
|
||||
|
||||
@@ -192,7 +192,7 @@ public:
|
||||
bool standalone_wifi = false);
|
||||
~WebConfigServer();
|
||||
|
||||
// Infrastructure roles refresh this from the actual MQTT worker state.
|
||||
// Infrastructure roles refresh this from actual MQTT/UDP station owners.
|
||||
// A compiled-in but stopped/unconfigured bridge cannot reconnect WiFi.
|
||||
// Companion runtimes retain their own station ownership. Loop task only.
|
||||
void updateWiFiOwnership(bool owns_wifi) {
|
||||
@@ -204,6 +204,10 @@ public:
|
||||
_setup_reconnect_in_progress = false;
|
||||
_setup_reconnect_deadline = 0;
|
||||
}
|
||||
// A station-only owner may reclaim an idle automatic recovery AP. Explicit
|
||||
// setup APs and active browser/OTA work keep their link until released.
|
||||
// Returns true when that owner may manage STA. Loop task only.
|
||||
bool prepareStationOwner();
|
||||
|
||||
// For the device display: true while a setup-mode portal is active.
|
||||
// Fills the AP SSID and portal IP; either buffer may be NULL to just poll.
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ struct MyMesh {
|
||||
const char* getRole() const { return "test"; }
|
||||
bool startWebConfig(bool force_ap, char* reply);
|
||||
bool startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup);
|
||||
bool webConfigOwnsWiFi() const;
|
||||
void suspendUnconfiguredSetupBridges();
|
||||
void refresh() {
|
||||
@REFRESH@
|
||||
|
||||
+93
-5
@@ -7,10 +7,12 @@
|
||||
#include <helpers/RoomWebMailbox.h>
|
||||
#include <helpers/WiFiReconnectPolicy.h>
|
||||
#include <helpers/WiFiNetworkPowerPolicy.h>
|
||||
#define MESH_MANAGEMENT_PASSWORD_HOST_TEST
|
||||
#include <helpers/esp32/ManagementPassword.h>
|
||||
static uint32_t clock_ms = 1000;
|
||||
uint32_t millis() { return clock_ms; }
|
||||
void delay(unsigned) {}
|
||||
enum { WL_DISCONNECTED = 6, WL_CONNECTED = 3, WIFI_STA = 1,
|
||||
enum { WL_DISCONNECTED = 6, WL_CONNECTED = 3, WIFI_STA = 1, WIFI_AP = 2, WIFI_AP_STA = 3,
|
||||
WIFI_IF_STA = 0, ESP_OK = 0, WIFI_PS_NONE = 0,
|
||||
WIFI_PS_MAX_MODEM = 2, WIFI_PS_MIN_MODEM = 1 };
|
||||
using wifi_ps_type_t = int;
|
||||
@@ -22,12 +24,16 @@ struct IP {
|
||||
};
|
||||
struct FakeWiFi {
|
||||
int state = WL_DISCONNECTED, begins = 0, disconnects = 0;
|
||||
int current_mode = WIFI_STA, ap_clients = 0, ap_disconnects = 0;
|
||||
bool fail_mode = false, fail_ap_stop = false;
|
||||
bool next_begin_connects = false;
|
||||
std::string ssid, password;
|
||||
int status() const { return state; }
|
||||
bool mode(int) { return true; }
|
||||
bool mode(int value) { if (fail_mode) return false; current_mode = value; return true; }
|
||||
int getMode() const { return current_mode; }
|
||||
int softAPgetStationNum() const { return ap_clients; }
|
||||
void disconnect(bool, bool) { ++disconnects; state = WL_DISCONNECTED; }
|
||||
void softAPdisconnect(bool) {}
|
||||
void softAPdisconnect(bool) { ++ap_disconnects; if (!fail_ap_stop) current_mode &= ~WIFI_AP; }
|
||||
IP localIP() const { return {}; }
|
||||
} WiFi;
|
||||
namespace mesh {
|
||||
@@ -39,7 +45,8 @@ Control& control() { static Control c; return c; }
|
||||
namespace wifi {
|
||||
constexpr uint8_t kPowerSaveMax = 2, kDefaultPowerSave = 0, kPowerSaveNone = 1;
|
||||
constexpr bool kPrimaryEspNowRadio = false;
|
||||
int applyProtocolMask(int) { return ESP_OK; }
|
||||
bool fail_protocol = false;
|
||||
int applyProtocolMask(int) { return fail_protocol ? -1 : ESP_OK; }
|
||||
void setStationAutoReconnect(bool) {}
|
||||
int beginStation(const char* ssid, const char* password) {
|
||||
++WiFi.begins; WiFi.ssid = ssid; WiFi.password = password;
|
||||
@@ -77,6 +84,7 @@ static bool canonical_available = true;
|
||||
static const char* canonical_ssid = "saved-station";
|
||||
static std::string canonical_password(64, 'A');
|
||||
struct DNS { void stop() {} };
|
||||
struct WCLock { explicit WCLock(void*) {} };
|
||||
class WebConfigServer {
|
||||
public:
|
||||
enum Mode { MODE_OFF, MODE_SETUP, MODE_CONNECTING, MODE_LAN };
|
||||
@@ -98,6 +106,13 @@ public:
|
||||
bool _setup_reconnect_in_progress = false, _board_cmds_probed = false;
|
||||
bool _was_setup_ap = false, _initial_setup = false;
|
||||
bool foreground = false;
|
||||
bool _setup_provisioning_privilege = false, _setup_wifi_handoff_pending = false;
|
||||
enum { BATCH_IDLE, BATCH_PENDING };
|
||||
int _batch_state = BATCH_IDLE;
|
||||
unsigned _reboot_at = 0, handler_refs = 0;
|
||||
bool routes_attached = true, handler_arrives_on_detach = false;
|
||||
struct Terminal { bool running = false; struct { bool pending = false; } queue; };
|
||||
Terminal* _terminal = nullptr;
|
||||
uint32_t _setup_reconnect_deadline = 0, _connect_deadline = 0;
|
||||
uint32_t _setup_started_at = 0, _last_activity = 0;
|
||||
WiFiReconnectPolicy::Tracker _wifi_reconnect_tracker;
|
||||
@@ -124,6 +139,7 @@ public:
|
||||
}
|
||||
bool startSetupMode(char* reply) {
|
||||
++setup_starts; WiFi.disconnect(false, true); _mode = MODE_SETUP;
|
||||
WiFi.current_mode = WIFI_AP_STA;
|
||||
createServer();
|
||||
_was_setup_ap = true; _retry_saved_wifi_in_setup = false;
|
||||
strcpy(reply, "setup"); return true;
|
||||
@@ -131,11 +147,15 @@ public:
|
||||
bool startLanMode(char* reply) { _mode = MODE_LAN; strcpy(reply, "LAN"); return true; }
|
||||
bool startAutoMode(char*);
|
||||
bool beginSavedStation(uint32_t);
|
||||
bool prepareStationOwner();
|
||||
void tick(uint32_t);
|
||||
void createServer() { ++server_starts; }
|
||||
void probeBoardCommands() { _board_cmds_probed = true; }
|
||||
void serviceTerminal(uint32_t) {}
|
||||
void serviceRoom(uint32_t) {}
|
||||
unsigned handlerRefCount() const { return handler_refs; }
|
||||
void detachRoutes() { routes_attached = false; if (handler_arrives_on_detach) ++handler_refs; }
|
||||
void attachRoutes() { routes_attached = true; }
|
||||
bool isNetworkForegroundActive(uint32_t) const { return foreground || _mode == MODE_SETUP; }
|
||||
bool bluetoothWiFiCoexistenceRequired() { return false; }
|
||||
bool acceptWiFi(Entry& e, const char* value) {
|
||||
@@ -153,6 +173,7 @@ public:
|
||||
};
|
||||
@CONSTRUCTOR@
|
||||
@ATTEMPT@
|
||||
@PREPARE@
|
||||
@START@
|
||||
@RECOVERY@
|
||||
|
||||
@@ -164,7 +185,74 @@ int main() {
|
||||
assert(portal._standalone_wifi && portal.rawPskCapability());
|
||||
assert(strcmp(portal._wifi_ssid, canonical_ssid) == 0);
|
||||
assert(strlen(portal._wifi_password) == 64);
|
||||
#if defined(TEST_CREDENTIAL_PRIORITY)
|
||||
#if defined(TEST_UDP_STATION_OWNER)
|
||||
// UDP takes the station before the portal's first 15-second deadline.
|
||||
assert(portal.startAutoMode(reply) && WiFi.begins == 1);
|
||||
assert(portal.prepareStationOwner() && !portal._owns_wifi && !portal._connect_deadline);
|
||||
portal.tick(16000);
|
||||
assert(portal.setup_starts == 0 && WiFi.begins == 1);
|
||||
// With UDP off, ordinary WiFi-on owns its join and may expose recovery AP.
|
||||
portal.updateWiFiOwnership(true);
|
||||
portal.tick(16001);
|
||||
portal.tick(31001);
|
||||
assert(portal.setup_starts == 1 && portal._retry_saved_wifi_in_setup);
|
||||
const int begins = WiFi.begins, disconnects = WiFi.ap_disconnects;
|
||||
// A browser, queued command, handoff, reboot or active HTTP handler must
|
||||
// finish without its AP being taken away when UDP is enabled later.
|
||||
WiFi.ap_clients = 1; assert(!portal.prepareStationOwner()); WiFi.ap_clients = 0;
|
||||
portal._batch_state = WebConfigServer::BATCH_PENDING;
|
||||
assert(!portal.prepareStationOwner()); portal._batch_state = WebConfigServer::BATCH_IDLE;
|
||||
portal._setup_wifi_handoff_pending = true;
|
||||
assert(!portal.prepareStationOwner()); portal._setup_wifi_handoff_pending = false;
|
||||
portal._reboot_at = 1; assert(!portal.prepareStationOwner()); portal._reboot_at = 0;
|
||||
portal.handler_refs = 1; assert(!portal.prepareStationOwner()); portal.handler_refs = 0;
|
||||
portal.handler_arrives_on_detach = true;
|
||||
assert(!portal.prepareStationOwner()); portal.handler_arrives_on_detach = false; portal.handler_refs = 0;
|
||||
WebConfigServer::Terminal terminal;
|
||||
portal._terminal = &terminal; terminal.queue.pending = true;
|
||||
assert(!portal.prepareStationOwner()); terminal.queue.pending = false; terminal.running = true;
|
||||
assert(!portal.prepareStationOwner()); portal._terminal = nullptr;
|
||||
mesh::RoomWebMailbox room;
|
||||
uint8_t token[32] = {1}, digest[32] = {2};
|
||||
assert(room.submit(token, 1, digest, "test", 4, clock_ms) == mesh::RoomWebMailbox::Result::Accepted);
|
||||
portal._room_mailbox = &room;
|
||||
assert(!portal.prepareStationOwner());
|
||||
assert(room.begin() && !portal.prepareStationOwner()); portal._room_mailbox = nullptr;
|
||||
assert(portal.routes_attached && WiFi.ap_disconnects == disconnects);
|
||||
assert(mesh::esp32::managementPassword::initialize("admin-password"));
|
||||
mesh::esp32::managementPassword::Snapshot credential;
|
||||
assert(mesh::esp32::managementPassword::snapshot(credential));
|
||||
{
|
||||
mesh::esp32::managementPassword::UploadGuard upload(credential.generation);
|
||||
assert(upload.acquired() && !portal.prepareStationOwner());
|
||||
assert(WiFi.ap_disconnects == disconnects);
|
||||
}
|
||||
// An SDK failure must retain the recovery state and attached routes so a
|
||||
// later ordinary loop can complete the handoff, without a reboot or clear.
|
||||
WiFi.fail_ap_stop = true;
|
||||
assert(!portal.prepareStationOwner() && portal.routes_attached);
|
||||
assert(portal._mode == WebConfigServer::MODE_SETUP && portal._retry_saved_wifi_in_setup);
|
||||
WiFi.fail_ap_stop = false;
|
||||
mesh::wifi::fail_protocol = true;
|
||||
assert(!portal.prepareStationOwner() && portal.routes_attached);
|
||||
assert(portal._mode == WebConfigServer::MODE_SETUP && portal._retry_saved_wifi_in_setup);
|
||||
mesh::wifi::fail_protocol = false;
|
||||
assert(portal.prepareStationOwner() && portal._mode == WebConfigServer::MODE_CONNECTING);
|
||||
assert(!portal._retry_saved_wifi_in_setup && !portal._was_setup_ap && portal.routes_attached);
|
||||
assert(WiFi.current_mode == WIFI_STA && WiFi.begins == begins);
|
||||
// The external owner joins; the same portal follows the resulting LAN.
|
||||
WiFi.state = WL_CONNECTED; portal.tick(31002);
|
||||
assert(portal._mode == WebConfigServer::MODE_LAN);
|
||||
// An explicit setup AP is never reclassified as an automatic fallback.
|
||||
portal._mode = WebConfigServer::MODE_OFF;
|
||||
portal.startSetupMode(reply);
|
||||
assert(!portal._retry_saved_wifi_in_setup);
|
||||
const int explicit_disconnects = WiFi.ap_disconnects;
|
||||
assert(!portal.prepareStationOwner() && WiFi.current_mode == WIFI_AP_STA);
|
||||
assert(WiFi.ap_disconnects == explicit_disconnects);
|
||||
portal._stopping = true;
|
||||
assert(!portal.prepareStationOwner());
|
||||
#elif defined(TEST_CREDENTIAL_PRIORITY)
|
||||
// Credential saves and async capability stay in canonical storage across
|
||||
// both ownership transitions; raw PSKs never enter MQTTPrefs[64].
|
||||
strcpy(obs.wifi_ssid, "legacy-station"); strcpy(obs.wifi_password, "legacy-secret");
|
||||
|
||||
@@ -66,6 +66,7 @@ struct Board {bool ota=false;bool isOTAUpdateRunning()const{return ota;}};
|
||||
struct ObserverPrefs {const char*wifi_ssid="test";const char*wifi_password="";uint8_t wifi_power_save=1;};
|
||||
struct CLI {Board board;ObserverPrefs observer;Board*getBoard(){return &board;}const ObserverPrefs*getObserverPrefs(){return &observer;}};
|
||||
struct MQTT {bool running=false,stopping=false;bool isRunning()const{return running;}bool isStopping()const{return stopping;}};
|
||||
struct WebConfig {bool allowed=true;int prepares=0;bool prepareStationOwner(){++prepares;return allowed;}};
|
||||
struct UDPBridge {
|
||||
static int allocations;bool running=false;int loops=0,closes=0;
|
||||
uint32_t address=0,mask=0;
|
||||
@@ -84,6 +85,7 @@ struct MyMesh {
|
||||
NodePrefs _prefs;void* _mgr=nullptr;CLI _cli;FakeSettings udp_bridge_settings;
|
||||
UDPBridge*udp_bridge=nullptr;uint32_t udp_station_retry_at=0;bool udp_network_paused=false;
|
||||
MQTT* mqtt_bridge=nullptr;bool web_active=false,web_stopping=false;
|
||||
WebConfig* _webconfig=nullptr;
|
||||
~MyMesh(){delete udp_bridge;}
|
||||
bool isWebConfigActive(){return web_active;}
|
||||
bool isWebConfigStopping(){return web_stopping;}
|
||||
@@ -131,17 +133,20 @@ int main(){
|
||||
assert(node.setUDPBridgeNetworkState(true)&&node.udp_bridge->running);
|
||||
WiFi.state=WL_IDLE_STATUS;now_ms+=16000;const int before_owner=WiFi.begins;
|
||||
#ifdef WITH_WEBCONFIG
|
||||
node.web_active=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner&&!node.udp_bridge->running);
|
||||
node.web_active=false;node.web_stopping=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner);
|
||||
node.web_stopping=false;
|
||||
WebConfig portal;node._webconfig=&portal;portal.allowed=false;
|
||||
node.serviceUDPBridge();assert(WiFi.begins==before_owner&&!node.udp_bridge->running);
|
||||
assert(portal.prepares==1);
|
||||
portal.allowed=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner+1);
|
||||
node._webconfig=nullptr;now_ms+=16000;
|
||||
#endif
|
||||
const int before_mqtt=WiFi.begins;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
MQTT mqtt;node.mqtt_bridge=&mqtt;mqtt.running=true;node.serviceUDPBridge();
|
||||
assert(WiFi.begins==before_owner&&!node.udp_bridge->running);
|
||||
mqtt.running=false;mqtt.stopping=true;node.serviceUDPBridge();assert(WiFi.begins==before_owner);
|
||||
assert(WiFi.begins==before_mqtt&&!node.udp_bridge->running);
|
||||
mqtt.running=false;mqtt.stopping=true;node.serviceUDPBridge();assert(WiFi.begins==before_mqtt);
|
||||
mqtt.stopping=false;
|
||||
#endif
|
||||
node.serviceUDPBridge();assert(WiFi.begins==before_owner+1);
|
||||
node.serviceUDPBridge();assert(WiFi.begins==before_mqtt+1);
|
||||
WiFi.state=WL_CONNECTED;node.serviceUDPBridge();assert(node.udp_bridge->running);
|
||||
// A failed MQTT destination must not park a healthy independent LAN bridge.
|
||||
mesh::wifi::NetworkPowerPolicy policy;
|
||||
@@ -174,6 +179,49 @@ int main(){
|
||||
'''
|
||||
|
||||
class UDPBridgeIntegrationTest(unittest.TestCase):
|
||||
def test_webconfig_station_ownership_uses_live_udp_and_mqtt_intent(self):
|
||||
method = body((ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text(encoding='ascii'),
|
||||
'bool MyMesh::webConfigOwnsWiFi(')
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
struct Worker {
|
||||
bool running=false,stopping=false;
|
||||
bool isRunning()const{return running;}
|
||||
bool isStopping()const{return stopping;}
|
||||
};
|
||||
struct MyMesh {
|
||||
Worker* mqtt_bridge=nullptr; bool udp=false;
|
||||
bool isUDPBridgeNetworkEnabled()const{return udp;}
|
||||
bool webConfigOwnsWiFi()const;
|
||||
};
|
||||
@METHOD@
|
||||
int main(){
|
||||
MyMesh node;Worker mqtt;
|
||||
for(int udp=0;udp<2;++udp)for(int state=0;state<4;++state){
|
||||
node.udp=udp;node.mqtt_bridge=state?&mqtt:nullptr;
|
||||
mqtt.running=state==2;mqtt.stopping=state==3;
|
||||
bool expected=true;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
expected=state<2;
|
||||
#endif
|
||||
#ifdef WITH_UDP_BRIDGE
|
||||
expected=expected&&!udp;
|
||||
#endif
|
||||
assert(node.webConfigOwnsWiFi()==expected);
|
||||
}
|
||||
}
|
||||
'''.replace('@METHOD@', method)
|
||||
for flags in ((), ('WITH_MQTT_BRIDGE',), ('WITH_UDP_BRIDGE',),
|
||||
('WITH_MQTT_BRIDGE', 'WITH_UDP_BRIDGE')):
|
||||
with self.subTest(flags=flags), tempfile.TemporaryDirectory(prefix='udp-web-owner-') as tmp:
|
||||
path = Path(tmp)
|
||||
(path / 'test.cpp').write_text(fixture, encoding='ascii')
|
||||
binary = path / 'test'
|
||||
subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++11', '-Wall', '-Wextra', '-Werror',
|
||||
'-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie',
|
||||
*['-D' + flag + '=1' for flag in flags], str(path / 'test.cpp'), '-o', str(binary)], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
def test_wireless_backend_can_query_public_espnow_state_in_every_udp_owner_mix(self):
|
||||
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text(encoding='ascii')
|
||||
signature = 'bool isEspNowBridgeRunning() override'
|
||||
|
||||
@@ -21,11 +21,16 @@ class WebConfigStationOwnershipTest(unittest.TestCase):
|
||||
start = extract_braced(source, "bool MyMesh::startWebConfig(")
|
||||
start += "\n" + extract_braced(source, "bool MyMesh::startWebConfigImpl(")
|
||||
tick = source.index("_webconfig->tick(millis());")
|
||||
refresh_start = source.rfind("#ifdef WITH_MQTT_BRIDGE", 0, tick)
|
||||
refresh_end = source.index("#endif", refresh_start) + len("#endif")
|
||||
self.assertLess(refresh_end, tick)
|
||||
self.assertFalse(source[refresh_end:tick].strip())
|
||||
refresh = source[refresh_start:refresh_end]
|
||||
if role == "simple_repeater":
|
||||
start += "\n" + extract_braced(source, "bool MyMesh::webConfigOwnsWiFi(")
|
||||
refresh_start = source.rfind("_webconfig->updateWiFiOwnership(", 0, tick)
|
||||
refresh = source[refresh_start:tick].strip()
|
||||
else:
|
||||
refresh_start = source.rfind("#ifdef WITH_MQTT_BRIDGE", 0, tick)
|
||||
refresh_end = source.index("#endif", refresh_start) + len("#endif")
|
||||
self.assertLess(refresh_end, tick)
|
||||
self.assertFalse(source[refresh_end:tick].strip())
|
||||
refresh = source[refresh_start:refresh_end]
|
||||
self.assertIn("_webconfig->updateWiFiOwnership(", refresh)
|
||||
body = fixture.replace("@WORKER@", worker).replace("@START@", start)
|
||||
body = body.replace("@SUSPEND@", extract_braced(source,
|
||||
@@ -34,8 +39,9 @@ class WebConfigStationOwnershipTest(unittest.TestCase):
|
||||
self.compile_and_run(body, "-DWITH_MQTT_BRIDGE=1")
|
||||
# A stopped/unconfigured MQTT implementation used to keep
|
||||
# ownership forever merely because its code was compiled in.
|
||||
stale = re.sub(r"owns_wifi = !.*?;", "owns_wifi = false;", body,
|
||||
count=1, flags=re.DOTALL)
|
||||
stale = (body.replace("return owns_wifi;", "return false;", 1)
|
||||
if role == "simple_repeater" else re.sub(
|
||||
r"owns_wifi = !.*?;", "owns_wifi = false;", body, count=1, flags=re.DOTALL))
|
||||
self.assertNotEqual(body, stale)
|
||||
with self.assertRaises(AssertionError):
|
||||
self.compile_and_run(stale, "-DWITH_MQTT_BRIDGE=1")
|
||||
@@ -48,6 +54,7 @@ class WebConfigStationOwnershipTest(unittest.TestCase):
|
||||
start = extract_braced(source, "bool WebConfigServer::startAutoMode(")
|
||||
attempt = extract_braced(source, "bool WebConfigServer::beginSavedStation(")
|
||||
setter = extract_braced(header, "void updateWiFiOwnership(")
|
||||
prepare = extract_braced(source, "bool WebConfigServer::prepareStationOwner(")
|
||||
tick = extract_braced(source, "void WebConfigServer::tick(")
|
||||
# Execute the complete production join/fallback/retry/promotion blocks.
|
||||
# HTTP batching, display, and settings routes below them are irrelevant.
|
||||
@@ -60,6 +67,7 @@ class WebConfigStationOwnershipTest(unittest.TestCase):
|
||||
capability = re.search(r' doc\["wifi_psk64"\] = .*?;', source).group(0)
|
||||
return (fixture.replace("@CONSTRUCTOR@", constructor).replace("@START@", start)
|
||||
.replace("@SETTER@", setter).replace("@RECOVERY@", recovery)
|
||||
.replace("@PREPARE@", prepare)
|
||||
.replace("@ATTEMPT@", attempt).replace("@CREDENTIAL_BATCH@", batch)
|
||||
.replace("@PSK_CAPABILITY@", capability))
|
||||
|
||||
@@ -70,6 +78,10 @@ class WebConfigStationOwnershipTest(unittest.TestCase):
|
||||
self.compile_and_run(self.station_fixture(), "-DWITH_MQTT_BRIDGE=1",
|
||||
"-DTEST_EXTERNAL_OWNER=1")
|
||||
|
||||
def test_udp_claims_connecting_or_idle_recovery_ap_and_preserves_explicit_ap(self):
|
||||
self.compile_and_run(self.station_fixture(), "-DWITH_MQTT_BRIDGE=1",
|
||||
"-DTEST_UDP_STATION_OWNER=1")
|
||||
|
||||
def test_canonical_raw_psk_wins_and_legacy_mqtt_credentials_still_load(self):
|
||||
fixture = self.station_fixture()
|
||||
self.compile_and_run(fixture, "-DWITH_MQTT_BRIDGE=1",
|
||||
|
||||
Reference in new issue
Block a user