Public Access
When the connection breaks during a release download (issue #54), the device waits up to a minute for Wi-Fi, asks for the rest of the file with a Range request and carries on: the slot and the hash so far are kept. It gives up after three tries in a row that bring nothing. Nothing is kept across a restart. Carrying on asks for 64 KB free, not 80: between two connections of an install 78 KB is free, and the first attempt on the device was refused for that. update damage cut now breaks every connection after n bytes. Tried on the device: v0.23.0 installed over four connections, and a download that brought nothing given up with nothing switched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
124 lines
5.9 KiB
C++
124 lines
5.9 KiB
C++
#pragma once
|
|
|
|
#include <freertos/FreeRTOS.h>
|
|
|
|
#include <functional>
|
|
#include <string>
|
|
|
|
#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<void(bool)> 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
|