feat(ota): broadcast OTA start/fail milestones on the alert channel

An operator who triggers `ota update` via remote management only sees the
command's reply; the actual OTA runs ~2.5s later and reboots on success,
so the start/fail notifications land outside the reply window. Mirror the
key OTA milestones onto the configured alert channel (in addition to the
Serial log):

- START ("OTA update starting") from beginDeferredOtaUpdate(), i.e. while
  the loop still runs -- a flood queued at the deferred fire could never
  transmit before the flash blocks the loop / reboots on success.
- FAIL ("OTA aborted: ...") at both abort points (teardown barrier
  withheld flashing; preflight/download error).

Success has no message: a successful flash reboots into the new image, so
the node returning on the new version is the signal.

New MyMesh::otaAlert() gates on the `alert on/off` master switch and rides
the configured alert scope (AlertReporter::sendText -> sendChannel ->
resolveAlertScope); no-op when alerts are off or no channel is set. Only
these start/fail milestones -- routine slot connect/disconnect is
unaffected (stays in AlertReporter's fault logic). Documented in ALERTS.md.
Both observer firmwares build.
This commit is contained in:
agessaman committed 2026-07-19 07:51:52 -07:00
1 parent 686a2f240a
commit f68a0fb9e7
3 files changed
+37

No files matched your search

+10
View File
@@ -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: <reason>` (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 |
+7
View File
@@ -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
+20
View File
@@ -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;
}