diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 455b883b0..d1d7eca81 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -617,6 +617,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks bool startSharedEspNowBridgeIfReady() { if (espnow_bridge.isRunning()) return true; if (!_prefs.espnow_bridge_enabled) return false; + // Browser OTA pauses the bridges for the entire upload session. The + // periodic retry must not undo that pause on its next loop iteration. + if (_cli.getBoard()->isOTAUpdateRunning()) return false; if (mqtt_bridge && mqtt_bridge->isRunning() && !WiFi.isConnected()) return false; if (!millisHasNowPassed(shared_espnow_retry_at)) return false; shared_espnow_retry_at = millis() + 5000; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index df105a687..3d21f01f0 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -580,6 +580,8 @@ public: bool startSharedEspNowBridgeIfReady() { if (espnow_bridge.isRunning()) return true; if (!_prefs.espnow_bridge_enabled) return false; + // Keep the browser uploader's bridge pause in effect until OTA stops. + if (_cli.getBoard()->isOTAUpdateRunning()) return false; if (bridge && bridge->isRunning() && !WiFi.isConnected()) return false; if (!millisHasNowPassed(shared_espnow_retry_at)) return false; shared_espnow_retry_at = millis() + 5000; diff --git a/test/test_wifi_ota_start.py b/test/test_wifi_ota_start.py index 0bdc902a1..f1eda6d28 100644 --- a/test/test_wifi_ota_start.py +++ b/test/test_wifi_ota_start.py @@ -171,6 +171,61 @@ int main() { fixture = (ROOT / "test/fixtures/webconfig_ota_handoff.cpp").read_text() self.compile_and_run(fixture.replace("@METHODS@", methods)) + def test_background_bridge_retry_waits_until_ota_stops(self): + fixture = r''' +#include +#include +uint32_t now = 0; +uint32_t millis() { return now; } +bool millisHasNowPassed(uint32_t deadline) { return int32_t(now - deadline) >= 0; } +struct Board { + bool ota = true; + bool isOTAUpdateRunning() const { return ota; } +} board; +struct Cli { Board* getBoard() { return &board; } }; +struct Bridge { + unsigned starts = 0; + bool running = false; + bool isRunning() const { return running; } + void begin() { ++starts; running = true; } +}; +struct WiFiMock { bool isConnected() { return true; } } WiFi; +struct Mesh { + Cli _cli; + struct { bool espnow_bridge_enabled = true; } _prefs; + Bridge espnow_bridge; + Bridge* @MQTT_MEMBER@ = nullptr; + uint32_t shared_espnow_retry_at = 0; + void configureBridgeFilter(Bridge*) {} + @RETRY@ +}; +int main() { + Mesh mesh; + // Simulate repeated servicePostMeshLoop calls, spanning several retry windows. + for (now = 0; now <= 30000; now += 100) { + assert(!mesh.startSharedEspNowBridgeIfReady()); + assert(mesh.espnow_bridge.starts == 0); + } + board.ota = false; + assert(mesh.startSharedEspNowBridgeIfReady()); + assert(mesh.espnow_bridge.starts == 1); + assert(mesh.startSharedEspNowBridgeIfReady()); + assert(mesh.espnow_bridge.starts == 1); + mesh.espnow_bridge.running = false; + mesh._prefs.espnow_bridge_enabled = false; + now += 30000; + assert(!mesh.startSharedEspNowBridgeIfReady()); + assert(mesh.espnow_bridge.starts == 1); +} +''' + for role, mqtt_member in (("simple_repeater", "mqtt_bridge"), + ("simple_room_server", "bridge")): + with self.subTest(role=role): + source = (ROOT / "examples" / role / "MyMesh.h").read_text() + retry = extract_braced(source, "bool startSharedEspNowBridgeIfReady()") + self.compile_and_run(fixture.replace("@RETRY@", retry) + .replace("@MQTT_MEMBER@", mqtt_member)) + def test_full_companion_cli_reports_webconfig_stop_and_hardware_errors(self): source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() command = extract_braced(source, 'if (strcmp(command, "start ota") == 0')