feat(ota): implement deferred OTA update scheduling and handling
Added support for deferred OTA updates in the MyMesh class, allowing the system to schedule firmware updates to occur after a confirmation reply is sent. This change improves the user experience by ensuring that the update process does not block the main application loop, allowing for smoother operation during firmware updates.
This commit is contained in:
1 parent
dec668386a
commit
c1885fcc15
8 files changed
+136
-29
No files matched your search
@@ -1445,6 +1445,22 @@ void MyMesh::loop() {
|
||||
MESH_DEBUG_PRINTLN("Radio params restored");
|
||||
}
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_URL)
|
||||
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.
|
||||
setBridgeState(false);
|
||||
char ota_reply[160];
|
||||
if (!_cli.getBoard()->otaFromManifest(getFirmwareVer(), false, ota_reply)) {
|
||||
MESH_DEBUG_PRINTLN("ota update aborted: %s", ota_reply);
|
||||
setBridgeState(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// is pending dirty contacts write needed?
|
||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
||||
acl.save(_fs);
|
||||
|
||||
@@ -116,6 +116,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
#endif
|
||||
CayenneLPP telemetry;
|
||||
unsigned long set_radio_at, revert_radio_at;
|
||||
unsigned long _ota_update_at = 0; // deferred `ota update` fire time (0 = none scheduled)
|
||||
float pending_freq;
|
||||
float pending_bw;
|
||||
uint8_t pending_sf;
|
||||
@@ -306,6 +307,15 @@ public:
|
||||
bridge->setSlotPreset(slot, _prefs.mqtt_slot_preset[slot]);
|
||||
}
|
||||
|
||||
// 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"
|
||||
return true;
|
||||
}
|
||||
|
||||
int getQueueSize() override {
|
||||
return bridge ? bridge->getQueueSize() : 0;
|
||||
}
|
||||
|
||||
@@ -800,18 +800,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
// ota check -> report available build, do not flash
|
||||
// ota update -> download and flash, then reboot
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_URL)
|
||||
bool dry = (memcmp(command, "ota check", 9) == 0);
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
strcpy(reply, "ERR: WiFi not connected");
|
||||
} else if (memcmp(command, "ota check", 9) == 0) {
|
||||
// Check is synchronous so its result lands in this reply. Free the MQTT
|
||||
// bridge first: on a no-PSRAM board only ~70 KB heap is free with the
|
||||
// bridge up, and a third TLS connection (the manifest fetch) alongside the
|
||||
// two live MQTT sessions drives free heap to a few hundred bytes, which
|
||||
// truncates the read. The WiFi STA link survives end(); restore after.
|
||||
_callbacks->setBridgeState(false);
|
||||
_board->otaFromManifest(_callbacks->getFirmwareVer(), true, reply);
|
||||
_callbacks->setBridgeState(true);
|
||||
} else {
|
||||
// Free the MQTT bridge (TLS contexts + task) so the manifest parse and
|
||||
// OTA download have heap headroom; the WiFi STA link survives end().
|
||||
if (!dry) _callbacks->setBridgeState(false);
|
||||
bool ok = _board->otaFromManifest(_callbacks->getFirmwareVer(), dry, reply);
|
||||
// On success the board reboots and never returns here; on any abort
|
||||
// (already up to date, partition change, download error) bring the
|
||||
// bridge back up so the node resumes uplinking.
|
||||
if (!ok && !dry) _callbacks->setBridgeState(true);
|
||||
// Update is DEFERRED: the flash blocks the loop and then reboots, so it
|
||||
// must run only AFTER this reply has gone out over the mesh — otherwise
|
||||
// the requester never gets a confirmation. The app loop runs it shortly.
|
||||
if (_callbacks->beginDeferredOtaUpdate()) {
|
||||
strcpy(reply, "Beginning update... (node will reboot if successful)");
|
||||
} else {
|
||||
strcpy(reply, "ERR: online OTA not available");
|
||||
}
|
||||
}
|
||||
#else
|
||||
strcpy(reply, "ERR: online OTA not supported on this build");
|
||||
|
||||
@@ -288,6 +288,14 @@ public:
|
||||
restartBridge();
|
||||
};
|
||||
|
||||
// Schedule a pull-OTA firmware update to run shortly (from the app loop), after
|
||||
// the "Beginning update..." CLI reply has been transmitted. Deferred because the
|
||||
// flash blocks the loop and then reboots, so it can't run inline with the reply.
|
||||
// Returns true if scheduled. Default: not supported.
|
||||
virtual bool beginDeferredOtaUpdate() {
|
||||
return false;
|
||||
};
|
||||
|
||||
virtual int getQueueSize() {
|
||||
return 0; // no op by default
|
||||
};
|
||||
|
||||
@@ -71,6 +71,8 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
|
||||
#include <HTTPClient.h>
|
||||
#include <HTTPUpdate.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// Embedded CA bundle (produced by board_build.embed_files). Weak so non-bundle
|
||||
// builds still link; we check for presence at runtime.
|
||||
@@ -93,7 +95,44 @@ static void ota_extractHash(const char* s, char* out, size_t out_sz) {
|
||||
out[n] = 0;
|
||||
}
|
||||
|
||||
// Parameters handed to the worker task; lives on otaFromManifest()'s stack,
|
||||
// which stays valid because that function blocks until the worker signals done.
|
||||
struct OtaTaskArgs {
|
||||
ESP32Board* self;
|
||||
const char* current_ver;
|
||||
bool dry_run;
|
||||
char* reply;
|
||||
volatile bool result;
|
||||
volatile bool done;
|
||||
};
|
||||
|
||||
static void ota_task_entry(void* param) {
|
||||
OtaTaskArgs* a = static_cast<OtaTaskArgs*>(param);
|
||||
a->result = a->self->otaFromManifestImpl(a->current_ver, a->dry_run, a->reply);
|
||||
a->done = true; // on a successful `ota update` we reboot before reaching here
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
bool ESP32Board::otaFromManifest(const char* current_ver, bool dry_run, char reply[]) {
|
||||
// The TLS handshake (cert-bundle verify) + JSON parse / HTTPUpdate use far more
|
||||
// stack than the ~8 KB loop task offers — especially when reached via the deep
|
||||
// mesh-receive call chain (it overflows the loopTask canary). Run the work in a
|
||||
// dedicated 24 KB-stack task and block here until it finishes. The big stack is
|
||||
// freed when the task exits; on a successful update the chip reboots inside it.
|
||||
OtaTaskArgs args = { this, current_ver, dry_run, reply, false, false };
|
||||
TaskHandle_t handle = nullptr;
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(ota_task_entry, "ota", 24576, &args, 5, &handle, 1);
|
||||
if (ok != pdPASS) {
|
||||
strcpy(reply, "ERR: OTA task spawn failed");
|
||||
return false;
|
||||
}
|
||||
while (!args.done) {
|
||||
delay(50); // Arduino delay() yields to other tasks
|
||||
}
|
||||
return args.result;
|
||||
}
|
||||
|
||||
bool ESP32Board::otaFromManifestImpl(const char* current_ver, bool dry_run, char reply[]) {
|
||||
#if !defined(OTA_MANIFEST_URL) || !defined(OTA_VARIANT)
|
||||
strcpy(reply, "ERR: OTA not configured (build via build.sh)");
|
||||
return false;
|
||||
|
||||
@@ -152,6 +152,10 @@ public:
|
||||
|
||||
bool startOTAUpdate(const char* id, char reply[]) override;
|
||||
bool otaFromManifest(const char* current_ver, bool dry_run, char reply[]) override;
|
||||
// Heavy body (TLS + JSON / HTTPUpdate). Runs in a dedicated large-stack task
|
||||
// spawned by otaFromManifest() — public only so that task entry point can call
|
||||
// it; not meant to be invoked directly.
|
||||
bool otaFromManifestImpl(const char* current_ver, bool dry_run, char reply[]);
|
||||
|
||||
void setInhibitSleep(bool inhibit) {
|
||||
inhibit_sleep = inhibit;
|
||||
|
||||
@@ -754,27 +754,44 @@ void MQTTBridge::initializeWiFiInTask() {
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.setAutoConnect(true);
|
||||
|
||||
// Set up WiFi event handlers for better diagnostics and immediate disconnection detection
|
||||
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch(event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
||||
// Set flag to trigger NTP sync from loop() instead of doing it here
|
||||
if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
|
||||
s_wifi_disconnect_time = millis();
|
||||
MQTT_DEBUG_PRINTLN("WiFi disconnected: reason %d", s_wifi_disconnect_reason);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
// Set up WiFi event handlers for better diagnostics and immediate disconnection
|
||||
// detection. Register ONCE — the bridge is reused across restarts (e.g. stopped
|
||||
// for `ota check`/`ota update`, or `set mqtt…` reconfigure) and WiFi.onEvent()
|
||||
// never removes prior callbacks, so re-registering leaks handlers and duplicates
|
||||
// every log line.
|
||||
if (!_wifi_event_registered) {
|
||||
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||
switch(event) {
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
MQTT_DEBUG_PRINTLN("WiFi connected: %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
|
||||
// Set flag to trigger NTP sync from loop() instead of doing it here
|
||||
if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true;
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
s_wifi_disconnect_reason = info.wifi_sta_disconnected.reason;
|
||||
s_wifi_disconnect_time = millis();
|
||||
MQTT_DEBUG_PRINTLN("WiFi disconnected: reason %d", s_wifi_disconnect_reason);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
_wifi_event_registered = true;
|
||||
}
|
||||
|
||||
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
|
||||
// Only (re)start the WiFi association if it isn't already up. end() leaves the
|
||||
// STA link connected, so on a restart (e.g. after `ota check`) calling
|
||||
// WiFi.begin() again forces a needless disconnect/reconnect — which also races
|
||||
// the MQTT task's first DNS lookup (getaddrinfo fails until WiFi/DNS recovers).
|
||||
// When already connected, the deferred slot setup still fires in mqttTaskLoop()
|
||||
// because _ntp_synced persists across end() (only _slots_setup_done is reset).
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
|
||||
} else if (!_ntp_synced && !_ntp_sync_pending) {
|
||||
_ntp_sync_pending = true; // already connected but never synced — kick NTP now
|
||||
}
|
||||
|
||||
// NOTE: Slot setup is deferred until after NTP sync in mqttTaskLoop().
|
||||
// JWT-auth slots need valid timestamps for token creation, and connecting
|
||||
|
||||
@@ -180,6 +180,11 @@ private:
|
||||
bool _ntp_synced;
|
||||
bool _ntp_sync_pending; // Flag to trigger NTP sync from loop() instead of event handler
|
||||
bool _slots_setup_done; // Deferred: slots set up after NTP sync
|
||||
// WiFi.onEvent() handler registered once and never removed by end(); the bridge
|
||||
// object is reused across restarts, so re-registering would leak handlers and
|
||||
// duplicate every connect/disconnect log line. Inline-initialised so it survives
|
||||
// construction and is NOT reset by end().
|
||||
bool _wifi_event_registered = false;
|
||||
int _max_active_slots; // Runtime limit: 5 with PSRAM, 2 without
|
||||
|
||||
// Pending slot reconfigure: set from CLI (Core 1), processed by MQTT task (Core 0)
|
||||
|
||||
Reference in new issue
Block a user