Merge fix/mqtt-review-2026-09-09 into feat/abstract-networking
#54 lands first, so the network abstraction is reworked on top of the per-slot client ownership model instead of alongside it. Two resolutions are semantic, not textual: - Link-transition teardown goes through the ownership API. It called softDisconnect() on every started client, which bypassed client_state and ignored the typed result. A slot that is still Starting now gets a real stop: softDisconnect() returns immediately on a client that is not yet connected, so its attempt would otherwise complete against the old route and deliver a CONNECTED event indistinguishable from the new one's (F04). On a medium switch the old route can still be briefly usable, so that is not hypothetical. A connected client keeps the cheap bounded path, and a quarantined one is left alone. - NetworkLink::applyPowerPrefs() adopts WifiPowerSavePolicy. Its local `2 ? MAX : NONE` mapping would have reintroduced F11 and read the new stored value 3 (explicit `min`) as none. It also applies the setting when the link starts already associated, which is the case the bridge used to cover. The NTP probe from #54 keeps its validation and its one-attempt-per-server bound, but resolves and gates on the selected link rather than on WiFi, so it works on an Ethernet-preferred node.
This commit is contained in:
commit
7f28231b25
30 files changed
+3075
-451
No files matched your search
@@ -1734,6 +1734,19 @@ void MyMesh::loop() {
|
||||
// MQTT processing runs in a separate FreeRTOS task on Core 0, so we don't call bridge.loop() here
|
||||
mesh::Mesh::loop();
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// A timed-out stop keeps the bridge down until the MQTT task acknowledges it.
|
||||
// Release the withheld resources whenever that late ack lands, and restart
|
||||
// only a bridge that is still meant to be running.
|
||||
if (bridge && bridge->stopAcknowledgedLate()) {
|
||||
bridge->pollLateStopAck();
|
||||
if (_bridge_resume_pending && _prefs.bridge_enabled) {
|
||||
Serial.println("MQTT: stop acknowledged late - resuming bridge");
|
||||
setBridgeState(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_BRIDGE
|
||||
// bridge.loop() is now handled by FreeRTOS task on Core 0 - no need to call it here
|
||||
#endif
|
||||
@@ -1787,8 +1800,9 @@ void MyMesh::loop() {
|
||||
// resume the bridge instead of flashing under uncertain ownership.
|
||||
if (bridge && !bridge->canFlashAfterStop()) {
|
||||
Serial.println("OTA: aborted, MQTT stop did not complete cleanly - resuming bridge");
|
||||
otaAlert("OTA aborted: MQTT stop unclean, bridge resumed");
|
||||
setBridgeState(true);
|
||||
otaAlert(bridge->isRunning() ? "OTA aborted: MQTT stop unclean, bridge resumed"
|
||||
: "OTA aborted: MQTT stop unproven, bridge resumes when it completes");
|
||||
} else if (!_cli.getBoard()->otaFromManifest(getFirmwareVer(), false, ota_reply)) {
|
||||
Serial.print("OTA: aborted, resuming bridge - "); Serial.println(ota_reply);
|
||||
char ota_alert_msg[160];
|
||||
|
||||
@@ -137,6 +137,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
ESPNowBridge bridge;
|
||||
#elif defined(WITH_MQTT_BRIDGE)
|
||||
MQTTBridge* bridge;
|
||||
// begin() was refused after an unproven stop; restart once the task acknowledges.
|
||||
bool _bridge_resume_pending = false;
|
||||
#endif
|
||||
#ifdef WITH_SNMP
|
||||
MeshSNMPAgent _snmp_agent;
|
||||
@@ -351,6 +353,9 @@ public:
|
||||
#endif
|
||||
if (!bridge) return;
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (!enable) _bridge_resume_pending = false; // an explicit stop cancels a pending resume
|
||||
#endif
|
||||
if (enable == bridge->isRunning()) return;
|
||||
if (enable)
|
||||
{
|
||||
@@ -368,6 +373,7 @@ public:
|
||||
bridge->begin();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_alerter.setBridge(bridge);
|
||||
_bridge_resume_pending = !bridge->isRunning() && bridge->isStopUnproven();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
@@ -375,6 +381,7 @@ public:
|
||||
bridge->end();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_alerter.setBridge(nullptr);
|
||||
_bridge_resume_pending = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -400,6 +407,9 @@ public:
|
||||
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
|
||||
#endif
|
||||
bridge->begin();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_bridge_resume_pending = !bridge->isRunning() && bridge->isStopUnproven();
|
||||
#endif
|
||||
}
|
||||
|
||||
void restartBridgeSlot(int slot) override {
|
||||
|
||||
@@ -1549,6 +1549,19 @@ void MyMesh::loop() {
|
||||
// Check radio FIRST to ensure we don't miss incoming packets
|
||||
// MQTT processing can take time, so we prioritize radio reception
|
||||
mesh::Mesh::loop();
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// A timed-out stop keeps the bridge down until the MQTT task acknowledges it.
|
||||
// Release the withheld resources whenever that late ack lands, and restart
|
||||
// only a bridge that is still meant to be running.
|
||||
if (bridge && bridge->stopAcknowledgedLate()) {
|
||||
bridge->pollLateStopAck();
|
||||
if (_bridge_resume_pending && _prefs.bridge_enabled) {
|
||||
Serial.println("MQTT: stop acknowledged late - resuming bridge");
|
||||
setBridgeState(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// bridge.loop() is now handled by FreeRTOS task on Core 0 - no need to call it here
|
||||
#endif
|
||||
@@ -1644,14 +1657,18 @@ void MyMesh::loop() {
|
||||
drainOutbound(OTA_TX_DRAIN_TIMEOUT_MS);
|
||||
|
||||
bool may_flash = true;
|
||||
if (bridge) bridge->pollLateStopAck();
|
||||
if (bridge_was_running) {
|
||||
setBridgeState(false);
|
||||
// OTA must not write after a forced/timed-out MQTT shutdown: its TLS/heap
|
||||
// ownership is uncertain until a subsequent clean start/stop cycle.
|
||||
may_flash = bridge && bridge->canFlashAfterStop();
|
||||
if (!may_flash) {
|
||||
Serial.println("OTA: aborted, MQTT stop did not complete cleanly");
|
||||
}
|
||||
} else if (bridge && bridge->isStopUnproven()) {
|
||||
// Reads as stopped, but an unacknowledged MQTT task may still own TLS/client state.
|
||||
may_flash = false;
|
||||
}
|
||||
if (!may_flash) {
|
||||
Serial.println("OTA: aborted, MQTT stop did not complete cleanly");
|
||||
}
|
||||
|
||||
char ota_reply[160];
|
||||
|
||||
@@ -205,6 +205,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
MQTTBridge* bridge;
|
||||
// begin() was refused after an unproven stop; restart once the task acknowledges.
|
||||
bool _bridge_resume_pending = false;
|
||||
#endif
|
||||
#ifdef WITH_SNMP
|
||||
MeshSNMPAgent _snmp_agent;
|
||||
@@ -362,6 +364,9 @@ public:
|
||||
#endif
|
||||
if (!bridge) return;
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (!enable) _bridge_resume_pending = false; // an explicit stop cancels a pending resume
|
||||
#endif
|
||||
if (enable == bridge->isRunning()) return;
|
||||
if (enable)
|
||||
{
|
||||
@@ -385,6 +390,7 @@ public:
|
||||
bridge->begin();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_alerter.setBridge(bridge);
|
||||
_bridge_resume_pending = !bridge->isRunning() && bridge->isStopUnproven();
|
||||
#endif
|
||||
}
|
||||
else
|
||||
@@ -392,6 +398,7 @@ public:
|
||||
bridge->end();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_alerter.setBridge(nullptr);
|
||||
_bridge_resume_pending = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -416,6 +423,9 @@ public:
|
||||
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
|
||||
#endif
|
||||
bridge->begin();
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
_bridge_resume_pending = !bridge->isRunning() && bridge->isStopUnproven();
|
||||
#endif
|
||||
}
|
||||
|
||||
void restartBridgeSlot(int slot) override {
|
||||
|
||||
Reference in new issue
Block a user