Public Access
Updates from Gitea, step 2: the connection (check and list work on the device)
The roots (ISRG X1 and X2), an HTTPS client that reads the answer as a stream, GiteaReleases (the latest and a list of ten), the Update Service's requests, install from Gitea through the existing install path, the daily check's schedule, the Check for updates setting, and console commands: update check | list | status | install <tag>. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#include "services/gitea_releases.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform/https_get.h"
|
||||
#include "update_check.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
struct Guard {
|
||||
explicit Guard(SemaphoreHandle_t l) : l_(l) { xSemaphoreTake(l_, portMAX_DELAY); }
|
||||
~Guard() { xSemaphoreGive(l_); }
|
||||
SemaphoreHandle_t l_;
|
||||
};
|
||||
|
||||
std::string api(const std::string& what) { return std::string("/api/v1/repos/") + release::kGiteaRepo + "/releases" + what; }
|
||||
} // namespace
|
||||
|
||||
bool GiteaReleases::fetch(const std::string& path, size_t max, std::vector<release::Release>& out) {
|
||||
HttpsGet get(net::User::Updates);
|
||||
std::string why = get.open(release::kGiteaHost, path, "application/json");
|
||||
if (!why.empty()) {
|
||||
finish(false, why);
|
||||
return false;
|
||||
}
|
||||
release::ReleaseReader reader(max);
|
||||
std::unique_ptr<uint8_t[]> buf(new (std::nothrow) uint8_t[512]);
|
||||
if (!buf) {
|
||||
finish(false, "Not enough memory");
|
||||
return false;
|
||||
}
|
||||
for (;;) {
|
||||
int n = get.read(buf.get(), 512);
|
||||
if (n < 0) {
|
||||
finish(false, "The connection broke off");
|
||||
return false;
|
||||
}
|
||||
if (n == 0) break;
|
||||
reader.feed(reinterpret_cast<const char*>(buf.get()), n);
|
||||
if (!reader.ok()) break;
|
||||
}
|
||||
reader.end();
|
||||
if (!reader.ok() || !reader.complete()) {
|
||||
finish(false, "Gitea's answer isn't what was expected");
|
||||
return false;
|
||||
}
|
||||
out = reader.releases();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GiteaReleases::finish(bool ok, const std::string& error) {
|
||||
Guard g(lock_);
|
||||
status_ = ok ? Status::Done : Status::Failed;
|
||||
error_ = error;
|
||||
seq_++;
|
||||
}
|
||||
|
||||
bool GiteaReleases::fetchLatest() {
|
||||
{
|
||||
Guard g(lock_);
|
||||
status_ = Status::Busy;
|
||||
error_.clear();
|
||||
}
|
||||
std::vector<release::Release> got;
|
||||
if (!fetch(api("/latest"), 1, got)) return false;
|
||||
if (got.empty() || !got[0].usable()) {
|
||||
finish(false, "No release to install on the server");
|
||||
return false;
|
||||
}
|
||||
{
|
||||
Guard g(lock_);
|
||||
latest_ = got[0];
|
||||
haveLatest_ = true;
|
||||
}
|
||||
finish(true, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GiteaReleases::fetchList() {
|
||||
{
|
||||
Guard g(lock_);
|
||||
status_ = Status::Busy;
|
||||
error_.clear();
|
||||
}
|
||||
std::vector<release::Release> got;
|
||||
if (!fetch(api("?limit=" + std::to_string(kListSize) + "&draft=false&pre-release=false"), kListSize, got)) return false;
|
||||
std::vector<release::Release> usable;
|
||||
for (auto& r : got)
|
||||
if (r.usable()) usable.push_back(std::move(r));
|
||||
{
|
||||
Guard g(lock_);
|
||||
list_ = std::move(usable);
|
||||
if (!list_.empty() && (!haveLatest_ || release::versionNewer(list_[0].tag, latest_.tag))) {
|
||||
latest_ = list_[0];
|
||||
haveLatest_ = true;
|
||||
}
|
||||
}
|
||||
finish(true, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
GiteaReleases::Status GiteaReleases::status() const {
|
||||
Guard g(lock_);
|
||||
return status_;
|
||||
}
|
||||
|
||||
std::string GiteaReleases::error() const {
|
||||
Guard g(lock_);
|
||||
return error_;
|
||||
}
|
||||
|
||||
uint32_t GiteaReleases::seq() const {
|
||||
Guard g(lock_);
|
||||
return seq_;
|
||||
}
|
||||
|
||||
bool GiteaReleases::latest(release::Release& out) const {
|
||||
Guard g(lock_);
|
||||
if (haveLatest_) out = latest_;
|
||||
return haveLatest_;
|
||||
}
|
||||
|
||||
std::vector<release::Release> GiteaReleases::list() const {
|
||||
Guard g(lock_);
|
||||
return list_;
|
||||
}
|
||||
|
||||
bool GiteaReleases::find(const std::string& tag, release::Release& out) const {
|
||||
Guard g(lock_);
|
||||
for (const auto& r : list_)
|
||||
if (r.tag == tag) return out = r, true;
|
||||
if (haveLatest_ && latest_.tag == tag) return out = latest_, true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "release_info.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// What the device knows of the project's releases on Gitea (docs/milestones/R1.md, #6): the latest,
|
||||
// and a list of the last ten. The fetching runs on the Update Service's task; the screens read what
|
||||
// came out, from the main loop.
|
||||
class GiteaReleases {
|
||||
public:
|
||||
enum class Status : uint8_t { Never, Busy, Done, Failed };
|
||||
static constexpr size_t kListSize = 10;
|
||||
|
||||
GiteaReleases() : lock_(xSemaphoreCreateMutex()) {}
|
||||
|
||||
// On the Update Service's task. True when the answer was read; otherwise error() says why.
|
||||
bool fetchLatest();
|
||||
bool fetchList();
|
||||
|
||||
Status status() const;
|
||||
std::string error() const;
|
||||
uint32_t seq() const; // grows with every answer that changed something
|
||||
bool latest(release::Release& out) const; // false: not fetched yet
|
||||
std::vector<release::Release> list() const;
|
||||
bool find(const std::string& tag, release::Release& out) const; // in the list, or the latest
|
||||
|
||||
private:
|
||||
bool fetch(const std::string& path, size_t max, std::vector<release::Release>& out);
|
||||
void finish(bool ok, const std::string& error);
|
||||
|
||||
mutable SemaphoreHandle_t lock_;
|
||||
Status status_ = Status::Never;
|
||||
std::string error_;
|
||||
uint32_t seq_ = 0;
|
||||
bool haveLatest_ = false;
|
||||
release::Release latest_;
|
||||
std::vector<release::Release> list_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <ctime>
|
||||
|
||||
#include "http_head.h"
|
||||
#include "platform/https_get.h"
|
||||
#include "platform/ota_device.h"
|
||||
#include "platform/system_info.h"
|
||||
#include "probation.h"
|
||||
@@ -60,6 +64,19 @@ class FileSource : public UpdateSource {
|
||||
File& f_;
|
||||
};
|
||||
|
||||
// A release being downloaded from Gitea: the same bytes a push or a card would give.
|
||||
class GiteaSource : public UpdateSource {
|
||||
public:
|
||||
explicit GiteaSource(HttpsGet& get) : get_(get) {}
|
||||
int read(uint8_t* buf, size_t len) override { return get_.read(buf, len); }
|
||||
|
||||
private:
|
||||
HttpsGet& get_;
|
||||
};
|
||||
|
||||
constexpr time_t kClockSetAfter = 1700000000; // anything earlier is the clock's default
|
||||
constexpr size_t kCheckFloor = 55 * 1024; // Q86, Q172
|
||||
|
||||
} // namespace
|
||||
|
||||
UpdateService::UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
|
||||
@@ -85,10 +102,11 @@ void UpdateService::start() {
|
||||
store_.getString("ota_from", from);
|
||||
if (!pending.empty() && pending != versionString()) {
|
||||
notify("Update to " + pending + " failed, back on " + versionString(), NotificationLevel::Warning);
|
||||
store_.putString("ota_failed", pending); // not announced again until there's a newer one (Q168)
|
||||
store_.putString("ota_pending", "");
|
||||
}
|
||||
|
||||
if (!task_) xTaskCreate(taskEntry, "update", 5120, this, 1, &task_); // peak 3.4 KB (ECDSA check, M2)
|
||||
if (!task_) xTaskCreate(taskEntry, "update", 8192, this, 1, &task_); // TLS to Gitea (#6): measured below
|
||||
}
|
||||
|
||||
void UpdateService::bootGuard(KeyValueStore& store) {
|
||||
@@ -106,6 +124,7 @@ void UpdateService::bootGuard(KeyValueStore& store) {
|
||||
}
|
||||
|
||||
void UpdateService::tick(uint32_t nowMs) {
|
||||
scheduleDailyCheck(nowMs);
|
||||
if (!probation_) return;
|
||||
bool wifiConfigured = settings_.getBool(Setting::WifiEnabled) && saved_.count() > 0;
|
||||
bool wifiUp = wifi_.state() == WifiController::State::Connected;
|
||||
@@ -184,6 +203,94 @@ void UpdateService::installFromSd(const std::string& path) {
|
||||
});
|
||||
}
|
||||
|
||||
std::string UpdateService::failedVersion() const {
|
||||
std::string failed;
|
||||
store_.getString("ota_failed", failed);
|
||||
return failed;
|
||||
}
|
||||
|
||||
std::string UpdateService::requestInstall(const std::string& tag, bool force) {
|
||||
if (phase_ != Phase::Idle || giteaBusy()) return "Busy with something else";
|
||||
if (!force && release::isDebugBuild(versionString())) return "A Debug Build keeps its console: update it from your PC";
|
||||
if (wifi_.state() != WifiController::State::Connected) return "No Wi-Fi";
|
||||
release::Release r;
|
||||
if (!gitea_.find(tag, r)) return "Look for releases first";
|
||||
if (!release::trustedAssetUrl(r.otaUrl)) return "That download isn't on the project's server";
|
||||
installTag_ = tag;
|
||||
request_ = Request::Install;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Once a day while Wi-Fi is up and the Clock is set (Q165). Skipped, and tried again later, when
|
||||
// something else is going on or memory is short; never during Probation or an install.
|
||||
void UpdateService::scheduleDailyCheck(uint32_t nowMs) {
|
||||
if (!dailyCheck_ || !settings_.getBool(Setting::CheckUpdates)) return;
|
||||
if (static_cast<int32_t>(nowMs - nextCheckMs_) < 0 || probation_ || phase_ != Phase::Idle || giteaBusy()) return;
|
||||
if (wifi_.state() != WifiController::State::Connected) return;
|
||||
time_t now = time(nullptr);
|
||||
if (now < kClockSetAfter) return;
|
||||
int32_t today = static_cast<int32_t>(now / 86400), last = 0;
|
||||
store_.getInt("rel_day", last);
|
||||
if (today == last) {
|
||||
nextCheckMs_ = nowMs + 3600000; // look again in an hour, in case the day has turned
|
||||
return;
|
||||
}
|
||||
if (esp_get_free_heap_size() < kCheckFloor) {
|
||||
nextCheckMs_ = nowMs + 600000;
|
||||
return;
|
||||
}
|
||||
nextCheckMs_ = nowMs + 1800000; // if this one fails
|
||||
request_ = Request::BackgroundCheck;
|
||||
}
|
||||
|
||||
void UpdateService::serve() {
|
||||
Request r = request_;
|
||||
if (r == Request::None) return;
|
||||
request_ = Request::None;
|
||||
serving_ = true;
|
||||
switch (r) {
|
||||
case Request::Check:
|
||||
case Request::BackgroundCheck: {
|
||||
if (!gitea_.fetchLatest()) break;
|
||||
time_t now = time(nullptr);
|
||||
if (now >= kClockSetAfter) store_.putInt("rel_day", static_cast<int32_t>(now / 86400));
|
||||
release::Release latest;
|
||||
if (r == Request::BackgroundCheck && gitea_.latest(latest) &&
|
||||
release::shouldAnnounce(latest, runningVersion(), failedVersion(), announced_)) {
|
||||
announced_ = latest.tag;
|
||||
notify("Update " + latest.tag + " available: see Settings > Firmware", NotificationLevel::Info);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Request::List: gitea_.fetchList(); break;
|
||||
case Request::Install: {
|
||||
release::Release release;
|
||||
if (gitea_.find(installTag_, release)) installFromGitea(release);
|
||||
break;
|
||||
}
|
||||
case Request::None: break;
|
||||
}
|
||||
serving_ = false;
|
||||
}
|
||||
|
||||
void UpdateService::installFromGitea(const release::Release& r) {
|
||||
release::Url url = release::parseUrl(r.otaUrl);
|
||||
incoming_ = r.tag;
|
||||
percent_ = 0;
|
||||
phase_ = Phase::Receiving;
|
||||
HttpsGet get(net::User::Updates);
|
||||
std::string why = get.open(url.host, url.path, "application/octet-stream");
|
||||
if (why.empty() && r.otaSize && get.contentLength() >= 0 && static_cast<uint32_t>(get.contentLength()) != r.otaSize)
|
||||
why = "The file isn't the size the server listed";
|
||||
if (!why.empty()) {
|
||||
phase_ = Phase::Idle;
|
||||
notify("Update refused: " + why, NotificationLevel::Warning);
|
||||
return;
|
||||
}
|
||||
GiteaSource source(get);
|
||||
install(source, "Gitea");
|
||||
}
|
||||
|
||||
void UpdateService::taskEntry(void* self) { static_cast<UpdateService*>(self)->listen(); }
|
||||
|
||||
void UpdateService::listen() {
|
||||
@@ -208,6 +315,7 @@ void UpdateService::listen() {
|
||||
esp_restart();
|
||||
}
|
||||
}
|
||||
if (phase_ == Phase::Idle) serve();
|
||||
if (listening && phase_ == Phase::Idle) {
|
||||
Counted<NetworkClient> client(server.accept(), net::User::Updates);
|
||||
if (client) {
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
|
||||
#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"
|
||||
|
||||
@@ -39,12 +43,34 @@ class UpdateService : public Service {
|
||||
// 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. A Debug Build doesn't install a release (Q171,
|
||||
// it would take its Debug Console away) unless `force`, which only the Debug Console passes.
|
||||
std::string requestInstall(const std::string& tag, bool force = false);
|
||||
bool giteaBusy() const { return request_ != Request::None || serving_; }
|
||||
// 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
|
||||
|
||||
// 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 };
|
||||
|
||||
void serve(); // on this task: one request
|
||||
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);
|
||||
@@ -62,6 +88,13 @@ class UpdateService : public Service {
|
||||
std::string incoming_;
|
||||
bool probation_ = false;
|
||||
volatile bool firstFrame_ = false;
|
||||
|
||||
GiteaReleases gitea_;
|
||||
volatile Request request_ = Request::None;
|
||||
volatile bool serving_ = false;
|
||||
std::string installTag_, fakeVersion_, announced_;
|
||||
bool dailyCheck_ = false;
|
||||
uint32_t nextCheckMs_ = 90000; // not before the device has settled
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
Reference in New Issue
Block a user