diff --git a/ALERTS.md b/ALERTS.md index 92b0974a..047eef0a 100644 --- a/ALERTS.md +++ b/ALERTS.md @@ -31,6 +31,16 @@ Alert floods ride the **repeater's default scope** by default (the same Transpor A "recovered" message is sent once when the underlying connection comes back. After firing, a fault is rate-limited by `alert.interval` (default 60 minutes) before it can re-fire - this prevents flapping links from spamming the channel. +## OTA milestone alerts + +Key `ota update` milestones are also broadcast on the alert channel (in addition to the Serial log), so an operator who triggered the update via remote management still gets feedback: the real OTA work runs ~2.5 s after the command and reboots on success, both **outside** the command's reply window. + +- **Start** - `OTA update starting`, sent when the update is confirmed and scheduled (during the reply window, so it transmits before the flash blocks the loop). +- **Fail/abort** - `OTA aborted: MQTT stop unclean, bridge resumed` (the teardown barrier withheld flashing) or `OTA aborted: ` (preflight/download error). The bridge is resumed either way. +- **Success** has no dedicated message: a successful flash reboots into the new image, so the node reappearing on the new version is the success signal. + +These share the alert channel, scope (`alert.region`), and the `alert` master switch with fault alerts - if `alert` is `off` or no channel is configured, OTA milestones are silent. Unlike fault alerts they are **not** rate-limited (OTA is operator-initiated and rare), and they are limited to these start/fail milestones - routine MQTT slot connect/disconnect never triggers an OTA alert. + ## Defaults | Setting | Default | Notes | diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a99d4de9..febcd5d5 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1575,11 +1575,18 @@ 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); } else if (!_cli.getBoard()->otaFromManifest(getFirmwareVer(), false, ota_reply)) { Serial.print("OTA: aborted, resuming bridge - "); Serial.println(ota_reply); + char ota_alert_msg[160]; + snprintf(ota_alert_msg, sizeof(ota_alert_msg), "OTA aborted: %s", ota_reply); + otaAlert(ota_alert_msg); setBridgeState(true); } + // Success path: otaFromManifest() flashes and reboots into the new image + // (never returns), so there is no in-boot "success" alert — the START alert + // plus the node returning on the new version is the success signal. } #endif diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index ee6b935d..f1ec48d2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -344,12 +344,32 @@ public: #endif } +#if defined(WITH_MQTT_BRIDGE) + // Broadcast a key OTA milestone (start/fail only) on the configured alert + // channel, in addition to the Serial log — so an operator who triggered + // `ota update` via remote management still gets feedback that lands well after + // the command's reply window. Respects the `alert on/off` master switch and + // rides the configured alert scope (sendChannel -> resolveAlertScope); a no-op + // when alerts are off or no channel is set. Deliberately NOT wired to routine + // slot connect/disconnect — those remain in AlertReporter's fault logic. + void otaAlert(const char* msg) { + auto* obs = _cli.getObserverPrefs(); + if (obs && obs->alert_enabled) _alerter.sendText(msg); + } +#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. bool beginDeferredOtaUpdate() override { _ota_update_at = millis() + 2500; if (_ota_update_at == 0) _ota_update_at = 1; // 0 means "none" +#if defined(WITH_MQTT_BRIDGE) + // Broadcast START now, while the loop still runs (the 2.5 s reply window): + // the deferred flash blocks the loop and, on success, reboots — so a start + // alert queued at fire time could never transmit. See otaAlert(). + otaAlert("OTA update starting"); +#endif return true; }