fix(ota): require clean MQTT stop before flashing

This commit is contained in:
agessaman committed 2026-07-21 16:46:02 -07:00
1 parent 5bf6d3a0e3
commit 36272384f3
2 files changed
+46 -7

No files matched your search

+32 -7
View File
@@ -23,6 +23,10 @@
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
// Best-effort bound for the queued CLI reply before OTA blocks the loop and
// reboots. Do not let a busy or duty-limited channel stall the update forever.
#define OTA_TX_DRAIN_TIMEOUT_MS 5000
#define LAZY_CONTACTS_WRITE_DELAY 5000
struct ServerStats {
@@ -1501,15 +1505,36 @@ void MyMesh::loop() {
#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_BASE)
if (_ota_update_at && millisHasNowPassed(_ota_update_at)) { // deferred `ota update`
_ota_update_at = 0; // clear timer
// The "Beginning update..." reply has now gone out. Free the bridge for heap
// headroom, then flash: otaFromManifest reboots into the new image on success
// (so this never returns); on any abort (already up to date, partition change,
// download error) it returns and we resume the bridge.
// The "Beginning update..." reply has now been queued. Flush it before OTA
// blocks the loop until reboot, then free a running bridge for heap headroom.
// Remember its state: an OTA request must not enable MQTT that an operator
// had deliberately stopped.
Serial.println("OTA: starting update");
setBridgeState(false);
const bool bridge_was_running = bridge && bridge->isRunning();
drainOutbound(OTA_TX_DRAIN_TIMEOUT_MS);
bool may_flash = true;
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");
}
}
char ota_reply[160];
if (!_cli.getBoard()->otaFromManifest(getFirmwareVer(), false, ota_reply)) {
Serial.print("OTA: aborted, resuming bridge - "); Serial.println(ota_reply);
if (may_flash && !_cli.getBoard()->otaFromManifest(getFirmwareVer(), false, ota_reply)) {
Serial.print("OTA: aborted - "); Serial.println(ota_reply);
may_flash = false;
}
// Successful otaFromManifest() reboots and never returns. Restore only a
// bridge that was running before this attempt; leave an intentionally
// stopped bridge stopped after any OTA refusal or download failure.
if (!may_flash && bridge_was_running) {
Serial.println("OTA: resuming bridge");
setBridgeState(true);
}
}
+14
View File
@@ -391,6 +391,20 @@ public:
#endif
}
#if defined(WITH_MQTT_BRIDGE)
// Pump already-queued mesh traffic before OTA blocks the loop and reboots.
// This is deliberately bounded: a jammed or duty-limited channel must not
// prevent an update, and Mesh::loop() continues to respect TX constraints.
void drainOutbound(uint32_t timeout_ms) {
unsigned long start = millis();
while (hasOutbound() || _mgr->getOutboundCount(millis()) > 0) {
if (millis() - start >= timeout_ms) break;
mesh::Mesh::loop();
delay(1);
}
}
#endif
// Schedule the pull-OTA flash to run from loop() in ~2.5 s, leaving time for the
// "Beginning update..." CLI reply (CLI_REPLY_DELAY_MILLIS = 600 ms) to transmit
// before the flash blocks the loop and reboots.