Files
roro9stack/src/services/update_service.h
T
twislaandClaude Opus 5.5 472f13260f
CI / build (pull_request) Successful in 2m6s
Site / build (pull_request) Successful in 11s
Updates: a download that breaks is carried on
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
2026-10-11 18:26:39 +02:00

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