#pragma once #include #include #include #include "update_check.h" #include "version.h" #include "event_bus.h" #include "key_value_store.h" #include "service.h" #include "services/gitea_releases.h" #include "services/storage_service.h" #include "services/wifi_service.h" namespace roro { // Firmware Updates (see CONTEXT.md and docs/milestones/OTA.md): listens for signed Update Files on // TCP 3232 while Wi-Fi is Connected, installs them from the SD card on request, keeps new firmware // on Probation until it proves healthy, and reports a Rollback after the reboot. class UpdateService : public Service { public: enum class Phase { Idle, Receiving, Installed, Failed }; static constexpr uint16_t kPort = 3232; // Call first in setup(): rolls back new firmware that already died once on Probation. static void bootGuard(KeyValueStore& store); UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage, EventBus& bus, const Settings& settings); const char* name() const override { return "update"; } uint32_t tickIntervalMs() const override { return 1000; } void start() override; void tick(uint32_t nowMs) override; // For the progress screen and the Firmware page. Phase phase() const { return phase_; } int percent() const { return percent_; } std::string incomingVersion() const; bool onProbation() const { return probation_; } // Installs an Update File from the SD card (runs on the storage task). void installFromSd(const std::string& path); // Updates from Gitea (docs/milestones/R1.md, #6). The requests are done on this Service's task; // the answers are read from gitea(). GiteaReleases& gitea() { return gitea_; } void requestCheck() { request_ = Request::Check; } void requestList() { request_ = Request::List; } // Downloads `tag` (a release known from the last check or list) into the inactive slot and // installs it. "" when it started, or why not. // `automatic`: nobody asked for it (issue #52), and the record of updates says so. std::string requestInstall(const std::string& tag, bool automatic = false); // A release the daily check found that may be installed without being asked for (the setting // is on): true once for each find, with its tag. The main loop picks the moment. bool takeOffer(std::string& tag); static constexpr const char* kRecordPath = "/system/updates.log"; // what was installed, and when bool giteaBusy() const { return request_ != Request::None || serving_; } // Asked to make room for a TLS connection (R1, Q172): IRC steps aside while the Update Service // talks to Gitea. Set by the main loop; `true` to step aside, `false` to come back. std::function holdMemory; // The daily check (Q165) only runs once the main loop says it may: not in Safe Mode. void enableDailyCheck() { dailyCheck_ = true; } // The version that counts as running for comparisons: a Debug Build can pretend to be older. std::string runningVersion() const { return fakeVersion_.empty() ? versionString() : fakeVersion_; } void pretendVersion(const std::string& v) { fakeVersion_ = v; } std::string failedVersion() const; // a release that rolled back here, "" if none // Debug Builds only, to try the refusals on the real network path: one HTTPS GET to any host // (is its certificate accepted?), and a download cut short or with one byte flipped. void requestProbe(const std::string& host, const std::string& path) { probeHost_ = host; probePath_ = path; probeResult_ = "..."; request_ = Request::Probe; } std::string probeResult() const { return probeResult_; } int lastResumes() const { return resumes_; } // how often the last download from Gitea was carried on (issue #54) void damageNextDownload(long cutAfter, long flipAt) { damageCut_ = cutAfter; damageFlip_ = flipAt; } void forgetToday() { store_.putInt("rel_day", 0); nextCheckMs_ = 0; announced_.clear(); } // the daily check runs at the next tick // The main loop calls this once it has drawn a frame (part of Probation). void firstFrameDrawn() { firstFrame_ = true; } // True when an installed update is waiting to reboot; the main loop reboots when it's safe. bool rebootPending() const { return phase_ == Phase::Installed; } private: enum class Request : uint8_t { None, Check, BackgroundCheck, List, Install, Probe }; void serve(); // on this task: one request bool makeRoom(); // true when a TLS connection can start; may have asked holdMemory void installFromGitea(const release::Release& release); void scheduleDailyCheck(uint32_t nowMs); // from tick() static void taskEntry(void* self); void listen(); void install(class UpdateSource& source, const char* via); void notify(const std::string& text, NotificationLevel level); void record(const std::string& what); KeyValueStore& store_; WifiService& wifi_; SavedNetworks& saved_; StorageService& storage_; EventBus& bus_; const Settings& settings_; TaskHandle_t task_ = nullptr; volatile Phase phase_ = Phase::Idle; volatile int percent_ = 0; std::string incoming_; bool probation_ = false; volatile bool firstFrame_ = false; GiteaReleases gitea_; volatile Request request_ = Request::None; volatile bool serving_ = false; bool held_ = false; // IRC was asked to step aside, on this task std::string installTag_, fakeVersion_, announced_, offered_; volatile bool offer_ = false; bool dailyCheck_ = false; uint32_t nextCheckMs_ = 90000; // not before the device has settled std::string probeHost_, probePath_; volatile long damageCut_ = -1, damageFlip_ = -1; volatile int resumes_ = 0; std::string probeResult_; }; } // namespace roro