diff --git a/src/devices/Device.hpp b/src/devices/Device.hpp index 982357bd..53ee5ec8 100644 --- a/src/devices/Device.hpp +++ b/src/devices/Device.hpp @@ -52,7 +52,7 @@ class ConsolePrinter : public Print { ConsolePrinter() { static const String spinner = "|/-\\"; static const int spinnerLength = spinner.length(); - Task::loop("console", 8192, 1, [this](Task& task) { + Task::loop("console", 2048, 1, [this](Task& task) { String status; counter = (counter + 1) % spinnerLength; @@ -199,7 +199,13 @@ class MqttTelemetryPublisher : public TelemetryPublisher { TelemetryCollector& telemetryCollector; }; -class Device : ConsoleProvider { +class ConfiguredKernel : ConsoleProvider { +public: + TDeviceDefinition deviceDefinition; + Kernel kernel { deviceDefinition.config, deviceDefinition.statusLed }; +}; + +class Device { public: Device() { @@ -277,9 +283,11 @@ class Device : ConsoleProvider { peripheralManager.publishTelemetry(); } - TDeviceDefinition deviceDefinition; + ConfiguredKernel configuredKernel; + Kernel& kernel = configuredKernel.kernel; + TDeviceDefinition& deviceDefinition = configuredKernel.deviceDefinition; TDeviceConfiguration& deviceConfig = deviceDefinition.config; - Kernel kernel { deviceConfig, deviceDefinition.statusLed }; + shared_ptr mqttDeviceRoot = kernel.mqtt.forRoot("devices/ugly-duckling/" + deviceConfig.instance.get()); PeripheralManager peripheralManager { mqttDeviceRoot }; @@ -293,7 +301,7 @@ class Device : ConsoleProvider { MemoryTelemetryProvider memoryTelemetryProvider; #endif - FileSystem& fs { FileSystem::get() }; + FileSystem& fs { kernel.fs }; EchoCommand echoCommand; RestartCommand restartCommand; SleepCommand sleepCommand; @@ -301,7 +309,9 @@ class Device : ConsoleProvider { FileReadCommand fileReadCommand { fs }; FileWriteCommand fileWriteCommand { fs }; FileRemoveCommand fileRemoveCommand { fs }; - HttpUpdateCommand httpUpdateCommand { kernel.version }; + HttpUpdateCommand httpUpdateCommand { [this](const String& url) { + kernel.prepareUpdate(url); + } }; }; } // namespace farmhub::devices diff --git a/src/kernel/Command.hpp b/src/kernel/Command.hpp index 00416ef0..7faff99f 100644 --- a/src/kernel/Command.hpp +++ b/src/kernel/Command.hpp @@ -5,13 +5,13 @@ #include #include -#include #include #include #include #include +#include using namespace std::chrono; @@ -200,9 +200,9 @@ class FileRemoveCommand : public FileCommand { class HttpUpdateCommand : public Command { public: - HttpUpdateCommand(const String& currentVersion) + HttpUpdateCommand(const std::function prepareUpdate) : Command("update") - , currentVersion(currentVersion) { + , prepareUpdate(prepareUpdate) { } void handle(const JsonObject& request, JsonObject& response) override { @@ -215,30 +215,17 @@ class HttpUpdateCommand : public Command { response["failure"] = "Command contains empty url"; return; } - httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - response["failure"] = update(url, currentVersion); + prepareUpdate(url); + response["success"] = true; + Task::run("update", [](Task& task) { + Log.infoln("Restarting in 5 seconds to apply update"); + delay(5000); + ESP.restart(); + }); } private: - static String update(const String& url, const String& currentVersion) { - Log.infoln("Updating from version %s via URL %s", - currentVersion.c_str(), url.c_str()); - WiFiClientSecure client; - // Allow insecure connections for testing - client.setInsecure(); - HTTPUpdateResult result = httpUpdate.update(client, url, currentVersion); - switch (result) { - case HTTP_UPDATE_FAILED: - return httpUpdate.getLastErrorString() + " (" + String(httpUpdate.getLastError()) + ")"; - case HTTP_UPDATE_NO_UPDATES: - return "No updates available"; - case HTTP_UPDATE_OK: - return "Update OK"; - default: - return "Unknown response"; - } - } - + const std::function prepareUpdate; const String currentVersion; }; diff --git a/src/kernel/Kernel.hpp b/src/kernel/Kernel.hpp index a0908753..5e8166f9 100644 --- a/src/kernel/Kernel.hpp +++ b/src/kernel/Kernel.hpp @@ -3,10 +3,13 @@ #include #include +#include + #include #include +#include #include #include #include @@ -25,6 +28,8 @@ class Kernel; static RTC_DATA_ATTR int bootCount = 0; +static const String UPDATE_FILE = "/update.json"; + // TODO Move this to a separate file static const String& getMacAddress() { static String macAddress; @@ -60,7 +65,13 @@ class Kernel { deviceConfig.instance.get().c_str(), deviceConfig.getHostname()); - Task::loop("status-update", 4096, [this](Task&) { updateState(); }); + Task::loop("status-update", 2048, [this](Task&) { updateState(); }); + + httpUpdateResult = handleHttpUpdate(); + } + + const State& getNetworkReadyState() const { + return networkReadyState; } const State& getRtcInSyncState() const { @@ -71,8 +82,22 @@ class Kernel { return kernelReadyState; } + const String& getHttpUpdateResult() const { + return httpUpdateResult; + } + + void prepareUpdate(const String& url) { + auto fUpdate = fs.open(UPDATE_FILE, FILE_WRITE); + DynamicJsonDocument doc(docSizeFor(url)); + doc["url"] = url; + serializeJson(doc, fUpdate); + fUpdate.close(); + } + const String version; + FileSystem& fs { FileSystem::get() }; + private: enum class KernelState { BOOTING, @@ -146,6 +171,60 @@ class Kernel { stateManager.awaitStateChange(); } + String handleHttpUpdate() { + if (!fs.exists(UPDATE_FILE)) { + return ""; + } + + Log.infoln("Starting update..."); + auto fUpdate = fs.open(UPDATE_FILE, FILE_READ); + DynamicJsonDocument doc(farmhub::kernel::docSizeFor(fUpdate)); + auto error = deserializeJson(doc, fUpdate); + fUpdate.close(); + fs.remove(UPDATE_FILE); + + if (error) { + return "Failed to parse update.json: " + String(error.c_str()); + } + String url = doc["url"]; + if (url.length() == 0) { + return "Command contains empty url"; + } + + Log.traceln("Waiting for network..."); + if (!networkReadyState.awaitSet(seconds(60))) { + return "Network not ready, aborting update"; + } + + httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + Log.infoln("Updating from version %s via URL %s", + VERSION, url.c_str()); + + HTTPUpdateResult result = HTTP_UPDATE_NO_UPDATES; + // Run in separate task to allocate enough stack + SemaphoreHandle_t completionSemaphore = xSemaphoreCreateBinary(); + Task::run("update", 8192, [&](Task& task) { + // Allocate on heap to avoid wasting stack + std::unique_ptr client = std::make_unique(); + // Allow insecure connections for testing + client->setInsecure(); + result = httpUpdate.update(*client, url, VERSION); + xSemaphoreGive(completionSemaphore); + }); + xSemaphoreTake(completionSemaphore, portMAX_DELAY); + + switch (result) { + case HTTP_UPDATE_FAILED: + return httpUpdate.getLastErrorString() + " (" + String(httpUpdate.getLastError()) + ")"; + case HTTP_UPDATE_NO_UPDATES: + return "No updates available"; + case HTTP_UPDATE_OK: + return "Update OK"; + default: + return "Unknown response"; + } + } + TDeviceConfiguration& deviceConfig; LedDriver& statusLed; @@ -171,6 +250,8 @@ class Kernel { MdnsDriver mdns { networkReadyState, deviceConfig.getHostname(), "ugly-duckling", version, mdnsReadyState }; RtcDriver rtc { networkReadyState, mdns, deviceConfig.ntp.get(), rtcInSyncState }; + String httpUpdateResult; + public: MqttDriver mqtt { networkReadyState, mdns, deviceConfig.mqtt.get(), deviceConfig.instance.get(), mqttReadyState }; };