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:
@@ -23,7 +23,7 @@ const RowDef kRows[] = {
|
|||||||
{Row::Gnss, Kind::Toggle, "GNSS"}, {Row::GnssQuiet, Kind::Toggle, "Pause GNSS for LoRa"},
|
{Row::Gnss, Kind::Toggle, "GNSS"}, {Row::GnssQuiet, Kind::Toggle, "Pause GNSS for LoRa"},
|
||||||
{Row::Coordinates, Kind::Toggle, "Coordinates"},
|
{Row::Coordinates, Kind::Toggle, "Coordinates"},
|
||||||
{Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Wifi, Kind::Page, "Wi-Fi"},
|
{Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Wifi, Kind::Page, "Wi-Fi"},
|
||||||
{Row::Firmware, Kind::Page, "Firmware"},
|
{Row::CheckUpdates, Kind::Toggle, "Check for updates"}, {Row::Firmware, Kind::Page, "Firmware"},
|
||||||
{Row::About, Kind::Page, "About"},
|
{Row::About, Kind::Page, "About"},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ std::string SettingsMenu::value(int i) const {
|
|||||||
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
||||||
case Row::Gnss: return settings_.getBool(Setting::GnssEnabled) ? "On" : "Off";
|
case Row::Gnss: return settings_.getBool(Setting::GnssEnabled) ? "On" : "Off";
|
||||||
case Row::GnssQuiet: return settings_.getBool(Setting::GnssQuietForLora) ? "On" : "Off";
|
case Row::GnssQuiet: return settings_.getBool(Setting::GnssQuietForLora) ? "On" : "Off";
|
||||||
|
case Row::CheckUpdates: return settings_.getBool(Setting::CheckUpdates) ? "Daily" : "Off";
|
||||||
case Row::Coordinates: return settings_.getBool(Setting::CoordinatesDms) ? "Deg min sec" : "Decimal";
|
case Row::Coordinates: return settings_.getBool(Setting::CoordinatesDms) ? "Deg min sec" : "Decimal";
|
||||||
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
||||||
case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||||
@@ -128,6 +129,7 @@ void SettingsMenu::toggle(int i) {
|
|||||||
if (row(i) == Row::Sound) settings_.setBool(Setting::Sound, !settings_.getBool(Setting::Sound));
|
if (row(i) == Row::Sound) settings_.setBool(Setting::Sound, !settings_.getBool(Setting::Sound));
|
||||||
if (row(i) == Row::Gnss) settings_.setBool(Setting::GnssEnabled, !settings_.getBool(Setting::GnssEnabled));
|
if (row(i) == Row::Gnss) settings_.setBool(Setting::GnssEnabled, !settings_.getBool(Setting::GnssEnabled));
|
||||||
if (row(i) == Row::GnssQuiet) settings_.setBool(Setting::GnssQuietForLora, !settings_.getBool(Setting::GnssQuietForLora));
|
if (row(i) == Row::GnssQuiet) settings_.setBool(Setting::GnssQuietForLora, !settings_.getBool(Setting::GnssQuietForLora));
|
||||||
|
if (row(i) == Row::CheckUpdates) settings_.setBool(Setting::CheckUpdates, !settings_.getBool(Setting::CheckUpdates));
|
||||||
if (row(i) == Row::Coordinates) settings_.setBool(Setting::CoordinatesDms, !settings_.getBool(Setting::CoordinatesDms));
|
if (row(i) == Row::Coordinates) settings_.setBool(Setting::CoordinatesDms, !settings_.getBool(Setting::CoordinatesDms));
|
||||||
if (row(i) == Row::ProbeMacs) settings_.setBool(Setting::ProbeMacRaw, !settings_.getBool(Setting::ProbeMacRaw));
|
if (row(i) == Row::ProbeMacs) settings_.setBool(Setting::ProbeMacRaw, !settings_.getBool(Setting::ProbeMacRaw));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace roro {
|
|||||||
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
||||||
class SettingsMenu {
|
class SettingsMenu {
|
||||||
public:
|
public:
|
||||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, Firmware, About };
|
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, CheckUpdates, Firmware, About };
|
||||||
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
||||||
|
|
||||||
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const Definition kDefinitions[] = {
|
|||||||
{"ntp1", Kind::String, 0, "pool.ntp.org", 1, 63},
|
{"ntp1", Kind::String, 0, "pool.ntp.org", 1, 63},
|
||||||
{"ntp2", Kind::String, 0, "time.cloudflare.com", 0, 63},
|
{"ntp2", Kind::String, 0, "time.cloudflare.com", 0, 63},
|
||||||
{"gnss_quiet", Kind::Bool, 0, nullptr, 0, 1}, // off: GNSS stays on, as decided in M2 (Q58)
|
{"gnss_quiet", Kind::Bool, 0, nullptr, 0, 1}, // off: GNSS stays on, as decided in M2 (Q58)
|
||||||
|
{"check_updates", Kind::Bool, 1, nullptr, 0, 1}, // on: it only looks, and says so (R1, Q165)
|
||||||
};
|
};
|
||||||
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||||
"every Setting needs a definition");
|
"every Setting needs a definition");
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum class Setting : uint8_t {
|
|||||||
Ntp1, // string: the first NTP server, a host name or an IPv4 address (S1, Q110)
|
Ntp1, // string: the first NTP server, a host name or an IPv4 address (S1, Q110)
|
||||||
Ntp2, // string: the second, or empty
|
Ntp2, // string: the second, or empty
|
||||||
GnssQuietForLora, // bool: put the GNSS receiver in standby while the LoRa radio listens (issue #20)
|
GnssQuietForLora, // bool: put the GNSS receiver in standby while the LoRa radio listens (issue #20)
|
||||||
|
CheckUpdates, // bool: look for a newer release on Gitea once a day (R1, Q165)
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
#include "services/clock_service.h"
|
#include "services/clock_service.h"
|
||||||
#include "services/debug_console.h"
|
#include "services/debug_console.h"
|
||||||
#include "services/file_ops.h"
|
#include "services/file_ops.h"
|
||||||
|
#include "update_check.h"
|
||||||
#include "services/gemini_service.h"
|
#include "services/gemini_service.h"
|
||||||
#include "services/gnss_service.h"
|
#include "services/gnss_service.h"
|
||||||
#include "services/power_service.h"
|
#include "services/power_service.h"
|
||||||
@@ -182,6 +183,7 @@ void setup() {
|
|||||||
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
||||||
update = new UpdateService(nvs, *wifi, *savedNetworks, *storageService, bus, settings);
|
update = new UpdateService(nvs, *wifi, *savedNetworks, *storageService, bus, settings);
|
||||||
fileOps = new FileOps(*storageService, filesInUse);
|
fileOps = new FileOps(*storageService, filesInUse);
|
||||||
|
update->enableDailyCheck();
|
||||||
irc = new IrcService(nvs, "roro_" + identity::defaultShortName(), *wifi, *storageService, *clockService, bus);
|
irc = new IrcService(nvs, "roro_" + identity::defaultShortName(), *wifi, *storageService, *clockService, bus);
|
||||||
notifier = new Notifier(bus, settings);
|
notifier = new Notifier(bus, settings);
|
||||||
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
||||||
@@ -441,6 +443,64 @@ static void fileOpsStep() {
|
|||||||
else console.printf("%s: ok\n", name);
|
else console.printf("%s: ok\n", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `update ...`: the project's releases on Gitea (R1, #6). The answers come from the Update Service's
|
||||||
|
// task a moment later; updateStep() prints them.
|
||||||
|
static int updateWatch = 0; // 1: a check, 2: a list
|
||||||
|
|
||||||
|
static void printReleases(bool list) {
|
||||||
|
GiteaReleases& g = update->gitea();
|
||||||
|
if (g.status() == GiteaReleases::Status::Failed) return (void)console.printf("update: %s\n", g.error().c_str());
|
||||||
|
std::string running = update->runningVersion();
|
||||||
|
if (!list) {
|
||||||
|
release::Release r;
|
||||||
|
if (!g.latest(r)) return (void)console.println("update: nothing known yet");
|
||||||
|
console.printf("update: latest %s of %s, %u bytes: %s (running %s)\n", r.tag.c_str(), r.date().c_str(), (unsigned)r.otaSize,
|
||||||
|
release::isNewer(r, running) ? "newer" : "not newer", running.c_str());
|
||||||
|
console.printf("update: %s\n", r.notes.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (auto& r : g.list())
|
||||||
|
console.printf("update: %-8s %s %8u B %s\n", r.tag.c_str(), r.date().c_str(), (unsigned)r.otaSize, r.notes.substr(0, 60).c_str());
|
||||||
|
console.println("update: end of list");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateStep() {
|
||||||
|
if (!updateWatch || update->giteaBusy()) return;
|
||||||
|
printReleases(updateWatch == 2);
|
||||||
|
updateWatch = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateCommand(const String& args) {
|
||||||
|
if (args == "check" || args == "list") {
|
||||||
|
if (update->giteaBusy()) return (void)console.println("update: busy");
|
||||||
|
args == "check" ? update->requestCheck() : update->requestList();
|
||||||
|
updateWatch = args == "check" ? 1 : 2;
|
||||||
|
} else if (args == "status") {
|
||||||
|
GiteaReleases& g = update->gitea();
|
||||||
|
console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(),
|
||||||
|
update->failedVersion().empty() ? "none" : update->failedVersion().c_str(),
|
||||||
|
settings.getBool(Setting::CheckUpdates) ? "on" : "off", (unsigned)ESP.getFreeHeap());
|
||||||
|
console.printf("update: gitea %s %s\n", g.status() == GiteaReleases::Status::Done ? "done" : g.status() == GiteaReleases::Status::Failed ? "failed" : g.status() == GiteaReleases::Status::Busy ? "busy" : "never asked", g.error().c_str());
|
||||||
|
printReleases(false);
|
||||||
|
} else if (args.startsWith("install ")) {
|
||||||
|
String rest = args.substring(8);
|
||||||
|
bool force = rest.endsWith(" force");
|
||||||
|
if (force) rest.remove(rest.length() - 6);
|
||||||
|
#ifndef RORO_DEBUG
|
||||||
|
force = false; // only the Debug Console may install on a Debug Build, which a release isn't
|
||||||
|
#endif
|
||||||
|
std::string why = update->requestInstall(rest.c_str(), force);
|
||||||
|
console.printf("update: %s\n", why.empty() ? "installing" : why.c_str());
|
||||||
|
#ifdef RORO_DEBUG
|
||||||
|
} else if (args.startsWith("pretend ")) { // update pretend v0.9.0 | off: what the comparisons take as running
|
||||||
|
update->pretendVersion(args.substring(8) == "off" ? "" : args.substring(8).c_str());
|
||||||
|
console.printf("update: running %s\n", update->runningVersion().c_str());
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
console.println("update: check | list | status | install <tag>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void fileCommand(const String& line) {
|
static void fileCommand(const String& line) {
|
||||||
int space = line.indexOf(' ');
|
int space = line.indexOf(' ');
|
||||||
std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str();
|
std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str();
|
||||||
@@ -507,6 +567,7 @@ static const char* const kHelp =
|
|||||||
"gemini get <url> fetch a Gemini page and report header, size, certificate, heap\n"
|
"gemini get <url> fetch a Gemini page and report header, size, certificate, heap\n"
|
||||||
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
|
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
|
||||||
"install <path.ota> Update from SD\n"
|
"install <path.ota> Update from SD\n"
|
||||||
|
"update check | list | status | install <tag> the project's releases on Gitea\n"
|
||||||
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
|
||||||
#ifdef RORO_DEBUG
|
#ifdef RORO_DEBUG
|
||||||
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
|
||||||
@@ -571,6 +632,7 @@ static void runCommand(String line) {
|
|||||||
esp_log_level_set("*", static_cast<esp_log_level_t>(constrain(level, 0, 5)));
|
esp_log_level_set("*", static_cast<esp_log_level_t>(constrain(level, 0, 5)));
|
||||||
console.printf("log level: %d\n", constrain(level, 0, 5));
|
console.printf("log level: %d\n", constrain(level, 0, 5));
|
||||||
}
|
}
|
||||||
|
if (line.startsWith("update ")) updateCommand(line.substring(7));
|
||||||
if (line.startsWith("cp ") || line.startsWith("mv ") || line.startsWith("rm ") || line.startsWith("mkdir ") ||
|
if (line.startsWith("cp ") || line.startsWith("mv ") || line.startsWith("rm ") || line.startsWith("mkdir ") ||
|
||||||
line.startsWith("du ") || line == "cancel")
|
line.startsWith("du ") || line == "cancel")
|
||||||
fileCommand(line);
|
fileCommand(line);
|
||||||
@@ -1010,6 +1072,7 @@ static void loopPass() {
|
|||||||
if (noiseTest) noiseTest->step(now);
|
if (noiseTest) noiseTest->step(now);
|
||||||
#endif
|
#endif
|
||||||
noteStableOnce(now);
|
noteStableOnce(now);
|
||||||
|
updateStep();
|
||||||
fileOpsStep();
|
fileOpsStep();
|
||||||
uploadStep();
|
uploadStep();
|
||||||
printListingWhenReady();
|
printListingWhenReady();
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
// The roots this device trusts for what it fetches over HTTPS (docs/milestones/R1.md, Q162): the two
|
||||||
|
// ISRG roots Let's Encrypt chains end in, not the framework's bundle of about 130 CAs. Public
|
||||||
|
// certificates, from https://letsencrypt.org/certs/ (SHA-256 fingerprints below).
|
||||||
|
//
|
||||||
|
// ISRG Root X1 RSA 4096 valid until 2035-06-04
|
||||||
|
// 96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6
|
||||||
|
// ISRG Root X2 ECDSA P-384 valid until 2040-09-17
|
||||||
|
// 69:72:9B:8E:15:A8:6E:FC:17:7A:57:AF:B7:17:1D:FC:64:AD:D2:8C:2F:CA:8C:F1:50:7E:34:45:3C:CB:14:70
|
||||||
|
//
|
||||||
|
// If the server's CA ever changes, the next firmware has to carry the new root and come from the PC.
|
||||||
|
inline constexpr char kTrustedRootsPem[] =
|
||||||
|
"-----BEGIN CERTIFICATE-----\n"
|
||||||
|
"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\n"
|
||||||
|
"-----END CERTIFICATE-----\n"
|
||||||
|
"-----BEGIN CERTIFICATE-----\n"
|
||||||
|
"MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\n"
|
||||||
|
"CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\n"
|
||||||
|
"R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\n"
|
||||||
|
"MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\n"
|
||||||
|
"ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\n"
|
||||||
|
"EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n"
|
||||||
|
"+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\n"
|
||||||
|
"ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\n"
|
||||||
|
"AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\n"
|
||||||
|
"zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\n"
|
||||||
|
"tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n"
|
||||||
|
"/q4AaOeMSQ+2b1tbFfLn\n"
|
||||||
|
"-----END CERTIFICATE-----\n";
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "platform/https_get.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#include "platform/ca_roots.h"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr size_t kFloor = 55 * 1024; // Q86, Q172: no connection below this
|
||||||
|
constexpr time_t kClockSetAfter = 1700000000; // 2023-11: anything earlier is the clock's default
|
||||||
|
|
||||||
|
// What mbedTLS says in the way it says it, for the cases a person can act on.
|
||||||
|
std::string whyNotConnected(NetworkClientSecure& tls, const std::string& host) {
|
||||||
|
char text[100] = "";
|
||||||
|
int err = tls.lastError(text, sizeof text);
|
||||||
|
std::string detail = text;
|
||||||
|
if (detail.find("X509") != std::string::npos || detail.find("certificate") != std::string::npos)
|
||||||
|
return "The certificate of " + host + " isn't accepted";
|
||||||
|
if (err != 0 && !detail.empty()) return "TLS to " + host + ": " + detail;
|
||||||
|
return "Can't connect to " + host;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string HttpsGet::open(const std::string& host, const std::string& path, const char* accept) {
|
||||||
|
close();
|
||||||
|
if (time(nullptr) < kClockSetAfter) return "The clock isn't set yet: certificates can't be checked";
|
||||||
|
if (esp_get_free_heap_size() < kFloor) return "Not enough memory: close IRC or a Gemini page";
|
||||||
|
|
||||||
|
tls_.setCACert(kTrustedRootsPem);
|
||||||
|
tls_.setTimeout(15);
|
||||||
|
if (!tls_.connect(host.c_str(), 443)) return whyNotConnected(tls_, host);
|
||||||
|
|
||||||
|
raw_.reset(new (std::nothrow) uint8_t[kRaw]);
|
||||||
|
if (!raw_) return "Not enough memory";
|
||||||
|
tls_.print(("GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nUser-Agent: roro9stack/" + versionString() +
|
||||||
|
"\r\nAccept: " + accept + "\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n")
|
||||||
|
.c_str());
|
||||||
|
|
||||||
|
release::HttpHeadParser parser;
|
||||||
|
while (!parser.complete()) {
|
||||||
|
int n = readRaw(raw_.get(), kRaw);
|
||||||
|
if (n <= 0) return "The server " + host + " stopped answering";
|
||||||
|
size_t used = parser.feed(reinterpret_cast<const char*>(raw_.get()), n);
|
||||||
|
if (parser.failed()) return host + " didn't answer with HTTP";
|
||||||
|
if (parser.complete() && used < static_cast<size_t>(n)) early_.assign(reinterpret_cast<const char*>(raw_.get()) + used, n - used);
|
||||||
|
}
|
||||||
|
head_ = parser.head();
|
||||||
|
if (head_.status != 200) return host + " answered " + std::to_string(head_.status) + (head_.status / 100 == 3 ? " (a redirect)" : "");
|
||||||
|
left_ = head_.chunked ? -1 : head_.contentLength;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int HttpsGet::readRaw(uint8_t* into, size_t len) {
|
||||||
|
uint32_t since = millis();
|
||||||
|
while (!tls_.available()) {
|
||||||
|
if (!tls_.connected()) return 0;
|
||||||
|
if (millis() - since > kStallMs) return -1;
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
|
return tls_.read(into, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int HttpsGet::read(uint8_t* buf, size_t len) {
|
||||||
|
if (done_ || len == 0) return 0;
|
||||||
|
if (!head_.chunked && left_ == 0) return done_ = true, 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
// Raw bytes: first those that came with the head, then the connection's.
|
||||||
|
size_t want = head_.chunked ? std::min(len, kRaw) : len;
|
||||||
|
if (!head_.chunked && left_ > 0) want = std::min<size_t>(want, left_);
|
||||||
|
int n;
|
||||||
|
if (earlyAt_ < early_.size()) {
|
||||||
|
n = static_cast<int>(std::min(want, early_.size() - earlyAt_));
|
||||||
|
std::copy(early_.data() + earlyAt_, early_.data() + earlyAt_ + n, head_.chunked ? raw_.get() : buf);
|
||||||
|
earlyAt_ += n;
|
||||||
|
} else {
|
||||||
|
n = readRaw(head_.chunked ? raw_.get() : buf, want);
|
||||||
|
}
|
||||||
|
if (n < 0) return -1;
|
||||||
|
if (n == 0) { // the server closed: the end, if it's where it said it would be
|
||||||
|
done_ = true;
|
||||||
|
bool whole = head_.chunked ? chunks_.done() : left_ <= 0;
|
||||||
|
return whole ? 0 : -1;
|
||||||
|
}
|
||||||
|
if (!head_.chunked) {
|
||||||
|
if (left_ > 0) left_ -= n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
size_t out = chunks_.decode(raw_.get(), n, buf);
|
||||||
|
if (chunks_.failed()) return -1;
|
||||||
|
if (out > 0) return static_cast<int>(out);
|
||||||
|
if (chunks_.done()) return done_ = true, 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpsGet::close() {
|
||||||
|
tls_.stop();
|
||||||
|
raw_.reset();
|
||||||
|
std::string().swap(early_);
|
||||||
|
earlyAt_ = 0;
|
||||||
|
done_ = false;
|
||||||
|
head_ = release::HttpHead();
|
||||||
|
chunks_ = release::ChunkedDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <NetworkClientSecure.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "http_head.h"
|
||||||
|
#include "platform/counted_client.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
// One HTTPS GET, read as a stream: the connection checks the server's chain and name against the
|
||||||
|
// roots in ca_roots.h, the head is parsed, and the body comes out whole whether the server sent it
|
||||||
|
// with a length or in chunks. Used by the Update Service to read Gitea's answers and to download a
|
||||||
|
// release. Redirects aren't followed: the device only goes where it was told to (Q163).
|
||||||
|
class HttpsGet {
|
||||||
|
public:
|
||||||
|
explicit HttpsGet(net::User user) : tls_(user) {}
|
||||||
|
~HttpsGet() { close(); }
|
||||||
|
|
||||||
|
// Connects and reads the head. "" when a 200 is on its way, or why not, in words for the screen.
|
||||||
|
std::string open(const std::string& host, const std::string& path, const char* accept);
|
||||||
|
long contentLength() const { return head_.contentLength; } // -1: not known
|
||||||
|
|
||||||
|
// Body bytes into `buf`: how many, 0 at the end, -1 when the connection broke or stalled
|
||||||
|
// before the end.
|
||||||
|
int read(uint8_t* buf, size_t len);
|
||||||
|
void close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t kRaw = 1024;
|
||||||
|
static constexpr uint32_t kStallMs = 10000;
|
||||||
|
|
||||||
|
int readRaw(uint8_t* into, size_t len); // from the connection, waiting up to kStallMs
|
||||||
|
|
||||||
|
Counted<NetworkClientSecure> tls_;
|
||||||
|
release::HttpHead head_;
|
||||||
|
release::ChunkedDecoder chunks_;
|
||||||
|
std::unique_ptr<uint8_t[]> raw_;
|
||||||
|
std::string early_; // body bytes that arrived with the head
|
||||||
|
size_t earlyAt_ = 0;
|
||||||
|
long left_ = -1; // body bytes still to come, if the server said how many
|
||||||
|
bool done_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -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 <memory>
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#include "http_head.h"
|
||||||
|
#include "platform/https_get.h"
|
||||||
#include "platform/ota_device.h"
|
#include "platform/ota_device.h"
|
||||||
#include "platform/system_info.h"
|
#include "platform/system_info.h"
|
||||||
#include "probation.h"
|
#include "probation.h"
|
||||||
@@ -60,6 +64,19 @@ class FileSource : public UpdateSource {
|
|||||||
File& f_;
|
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
|
} // namespace
|
||||||
|
|
||||||
UpdateService::UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
|
UpdateService::UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
|
||||||
@@ -85,10 +102,11 @@ void UpdateService::start() {
|
|||||||
store_.getString("ota_from", from);
|
store_.getString("ota_from", from);
|
||||||
if (!pending.empty() && pending != versionString()) {
|
if (!pending.empty() && pending != versionString()) {
|
||||||
notify("Update to " + pending + " failed, back on " + versionString(), NotificationLevel::Warning);
|
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", "");
|
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) {
|
void UpdateService::bootGuard(KeyValueStore& store) {
|
||||||
@@ -106,6 +124,7 @@ void UpdateService::bootGuard(KeyValueStore& store) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UpdateService::tick(uint32_t nowMs) {
|
void UpdateService::tick(uint32_t nowMs) {
|
||||||
|
scheduleDailyCheck(nowMs);
|
||||||
if (!probation_) return;
|
if (!probation_) return;
|
||||||
bool wifiConfigured = settings_.getBool(Setting::WifiEnabled) && saved_.count() > 0;
|
bool wifiConfigured = settings_.getBool(Setting::WifiEnabled) && saved_.count() > 0;
|
||||||
bool wifiUp = wifi_.state() == WifiController::State::Connected;
|
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::taskEntry(void* self) { static_cast<UpdateService*>(self)->listen(); }
|
||||||
|
|
||||||
void UpdateService::listen() {
|
void UpdateService::listen() {
|
||||||
@@ -208,6 +315,7 @@ void UpdateService::listen() {
|
|||||||
esp_restart();
|
esp_restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (phase_ == Phase::Idle) serve();
|
||||||
if (listening && phase_ == Phase::Idle) {
|
if (listening && phase_ == Phase::Idle) {
|
||||||
Counted<NetworkClient> client(server.accept(), net::User::Updates);
|
Counted<NetworkClient> client(server.accept(), net::User::Updates);
|
||||||
if (client) {
|
if (client) {
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "update_check.h"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
#include "event_bus.h"
|
#include "event_bus.h"
|
||||||
#include "key_value_store.h"
|
#include "key_value_store.h"
|
||||||
#include "service.h"
|
#include "service.h"
|
||||||
|
#include "services/gitea_releases.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
#include "services/wifi_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).
|
// Installs an Update File from the SD card (runs on the storage task).
|
||||||
void installFromSd(const std::string& path);
|
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).
|
// The main loop calls this once it has drawn a frame (part of Probation).
|
||||||
void firstFrameDrawn() { firstFrame_ = true; }
|
void firstFrameDrawn() { firstFrame_ = true; }
|
||||||
// True when an installed update is waiting to reboot; the main loop reboots when it's safe.
|
// True when an installed update is waiting to reboot; the main loop reboots when it's safe.
|
||||||
bool rebootPending() const { return phase_ == Phase::Installed; }
|
bool rebootPending() const { return phase_ == Phase::Installed; }
|
||||||
|
|
||||||
private:
|
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);
|
static void taskEntry(void* self);
|
||||||
void listen();
|
void listen();
|
||||||
void install(class UpdateSource& source, const char* via);
|
void install(class UpdateSource& source, const char* via);
|
||||||
@@ -62,6 +88,13 @@ class UpdateService : public Service {
|
|||||||
std::string incoming_;
|
std::string incoming_;
|
||||||
bool probation_ = false;
|
bool probation_ = false;
|
||||||
volatile bool firstFrame_ = 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
|
} // namespace roro
|
||||||
|
|||||||
Reference in New Issue
Block a user