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:
2026-10-06 15:20:58 +02:00
co-authored by Claude Sonnet 5.5
parent b0e8226943
commit 5754ae5b57
12 changed files with 616 additions and 3 deletions
+109 -1
View File
@@ -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) {