Public Access
Compare commits
4
Commits
compass
...
auto-update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3fd9909a1 | ||
|
|
8ba0102910 | ||
|
|
17651cb911 | ||
|
|
5af695f43a |
@@ -107,6 +107,7 @@ With no PC and no card, the device can install the project's releases itself (do
|
||||
- **Latest release** checks the server (Enter, or `c`) and says `v0.11.0 (new)` or `(current)`. Enter again opens the release: its version, date, size and the tag's message, with **Install** when it's newer. The download goes straight into the inactive slot, so no card is needed; the signature is checked after the first 160 bytes, before anything is written, and the image's hash at the end. The new firmware then runs on Probation as for any update.
|
||||
- **Older releases** lists the last ten, newest first. Opening an older one offers to go back to it, with a different question.
|
||||
- **Settings → System → Check for updates** (on by default): once a day, with Wi-Fi up and the clock set, the device looks at the latest release and says `v0.11.0 is out: see Settings > System > Firmware`, once per version. It installs nothing by itself, and doesn't announce a version that already failed and rolled back on this device.
|
||||
- **Settings → System → Install updates** (`When asked` by default; `By itself`, issue #52): a newer release found by the daily check is installed without being asked for. The device waits until no key was pressed for 2 minutes and nothing would be cut by a restart (a Track, a Capture, a copy, an upload, an SSH session, a web share), then asks on the screen for 30 seconds, lighting it if it was off: Cancel leaves it until the next day's check; Accept, or no answer, installs it and restarts. What was installed and when, and what was undone, is written in `/system/updates.log` on the card.
|
||||
|
||||
The connection is checked against the two ISRG roots Let's Encrypt chains end in (ADR 0009), not the usual bundle of about 130 authorities. Whatever the connection, the Update File's own signature is what decides what gets installed.
|
||||
|
||||
|
||||
+33
-1
@@ -1,6 +1,6 @@
|
||||
# R1 — Releases
|
||||
|
||||
**Status:** in progress. Shipped: CI and signed releases on Gitea (issue #5), a release for every tag; updates from Gitea (#6, **v0.11.0**); one firmware with the Debug Console in it (#68, **v0.12.0**); CI in about a minute (#74, **v0.13.0**). Not started: the Issues App (#4), automatic installs (#52), release channels (#53), resuming a download (#54).
|
||||
**Status:** in progress. Shipped: CI and signed releases on Gitea (issue #5), a release for every tag; updates from Gitea (#6, **v0.11.0**); one firmware with the Debug Console in it (#68, **v0.12.0**); CI in about a minute (#74, **v0.13.0**). Built, not released yet: installing a release by itself (#52). Not started: the Issues App (#4), release channels (#53), resuming a download (#54).
|
||||
|
||||
**Goal:** a tag is a release, built the same way every time and published where a device can find it.
|
||||
|
||||
@@ -124,6 +124,38 @@ The device looks at the project's Gitea for a newer release, says so, and instal
|
||||
|
||||
**Two slips during the checks:** a blind sequence of keys on the Firmware page opened the SD card's install dialog (the page keeps its selection between visits); it was cancelled with Back, nothing installed. And my port-polling while waiting for a restart took the Debug Console's only client slot, which made the first install attempt look like a failure.
|
||||
|
||||
## Installing a release by itself (issue #52)
|
||||
|
||||
For a device that sits on a desk, "always on the latest" may be what is wanted. Until now the daily check only said that a release was out.
|
||||
|
||||
### Decisions (2026-10-11)
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| Q275 | **A setting, off by default:** Settings > System > Install updates, `When asked` or `By itself`. It changes what runs on the device without anyone looking, so it is never on unless the owner said so. |
|
||||
| Q276 | **The owner is asked, and silence is yes:** a question over whatever is open, `Cancel` or `Accept`, that answers itself after 30 seconds by installing. The screen lights up for it if it was off. (The owner's own answer on the issue.) |
|
||||
| Q277 | **The question comes before the download,** not before the restart: the download already takes the screen over and may take IRC offline for its memory, so that is the moment to ask. |
|
||||
| Q278 | **Only in a quiet moment:** no key for 2 minutes, and nothing a restart would cut: a Track, a Capture, a file operation, an upload, an scp transfer, an SSH session, a web share, a noise test, another update. If one of them starts while the question is up, the question goes and comes back later with a full count. |
|
||||
| Q279 | **Any newer release,** not only patch releases, and with no delay after it is published: Probation and Rollback are what protect against a bad one, and a version that rolled back here is never offered again. |
|
||||
| Q280 | **Cancel lasts a day:** the release is offered again at the next daily check, for as long as it is the newest. |
|
||||
| Q281 | **A record:** each update that held (and whether it installed by itself), and each one that was undone, is a dated line in `/system/updates.log` on the card. |
|
||||
|
||||
### As built
|
||||
|
||||
- **`lib/release/src/auto_update.h`**, host-tested (10 tests): the waiting, the question and its count, as a state machine that is told what "busy" and "idle" are.
|
||||
- **The Update Service** offers the release at each background check when the setting is on (`shouldInstall`), and writes the record when Probation confirms a version or a rollback is reported.
|
||||
- **The main loop** decides what busy is, puts the question up through `Screen::render`, and gives it every key while it is up.
|
||||
- `update status` says whether a release is waiting for its quiet moment.
|
||||
|
||||
### Checked on the device (2026-10-11)
|
||||
|
||||
With the setting on, `update pretend v0.22.0` and `update daily`:
|
||||
|
||||
- The question came up over the Settings App, counting down each second. **Cancel** took it down and nothing was installed.
|
||||
- Left unanswered, the device downloaded v0.23.0, installed it and restarted into it.
|
||||
- `/system/updates.log` got its line at each confirmed update.
|
||||
- Not checked: "by itself" in the record (the release installed did not have this code yet), the 2 minutes without a key and the busy cases (host tests only), and a screen that was off lighting up. Keys sent through the Debug Console do not count as keys.
|
||||
|
||||
## One firmware: the Debug Console in every build (issue #68)
|
||||
|
||||
The issue asked for a token that could be set, so that Debug Builds could be published. The design round ended somewhere simpler: no Debug Builds. The console is in every firmware, off until switched on, with a token that belongs to the device (ADR 0010, which supersedes part of ADR 0004).
|
||||
|
||||
@@ -130,7 +130,7 @@ Settings had grown to 21 rows in one list. It now opens on five groups, each a s
|
||||
| Display | Brightness, Dim after, Screen off after, Theme, Light or dark, Launcher |
|
||||
| GNSS and radio | GNSS, Pause GNSS for LoRa, Coordinates, Probe MACs |
|
||||
| Network | Wi-Fi, VPN |
|
||||
| System | Check for updates, Firmware, Debug Console, About |
|
||||
| System | Check for updates, Install updates, Firmware, Debug Console, About |
|
||||
|
||||
- Enter opens a group, Back returns to the groups with the selection on the one just left, and Back from the groups leaves Settings. An open group has its name over its rows.
|
||||
- `SettingsMenu` (`lib/apps_model`) holds the groups and which one is open; every index is into what is listed. Host-tested: every setting is in exactly one group, and no group has more rows than fit under its name.
|
||||
|
||||
@@ -48,6 +48,7 @@ const Member kRows[] = {
|
||||
{3, {Row::Wifi, Kind::Page, "Wi-Fi"}},
|
||||
{3, {Row::Vpn, Kind::Page, "VPN"}},
|
||||
{4, {Row::CheckUpdates, Kind::Toggle, "Check for updates"}},
|
||||
{4, {Row::InstallUpdates, Kind::Toggle, "Install updates"}},
|
||||
{4, {Row::Firmware, Kind::Page, "Firmware"}},
|
||||
{4, {Row::DebugConsole, Kind::Page, "Debug Console"}},
|
||||
{4, {Row::About, Kind::Page, "About"}},
|
||||
@@ -152,6 +153,7 @@ std::string SettingsMenu::value(int i) const {
|
||||
case Row::Gnss: return settings_.getBool(Setting::GnssEnabled) ? "On" : "Off";
|
||||
case Row::GnssQuiet: return settings_.getBool(Setting::GnssQuietForLora) ? "On" : "Off";
|
||||
case Row::CheckUpdates: return settings_.getBool(Setting::CheckUpdates) ? "Daily" : "Off";
|
||||
case Row::InstallUpdates: return settings_.getBool(Setting::InstallUpdates) ? "By itself" : "When asked";
|
||||
case Row::Coordinates: return settings_.getBool(Setting::CoordinatesDms) ? "Deg min sec" : "Decimal";
|
||||
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
||||
case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||
@@ -210,6 +212,7 @@ void SettingsMenu::toggle(int i) {
|
||||
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::CheckUpdates) settings_.setBool(Setting::CheckUpdates, !settings_.getBool(Setting::CheckUpdates));
|
||||
if (row(i) == Row::InstallUpdates) settings_.setBool(Setting::InstallUpdates, !settings_.getBool(Setting::InstallUpdates));
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace roro {
|
||||
// Rendering and navigation live in the App. Every index is into what is listed now.
|
||||
class SettingsMenu {
|
||||
public:
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Launcher, Theme, ThemeLight, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, CheckUpdates, Firmware, Vpn, DebugConsole, About,
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Launcher, Theme, ThemeLight, Sound, Gnss, GnssQuiet, Coordinates, ProbeMacs, Wifi, CheckUpdates, InstallUpdates, Firmware, Vpn, DebugConsole, About,
|
||||
GroupDevice, GroupDisplay, GroupPosition, GroupNetwork, GroupSystem };
|
||||
enum class Kind { Text, Choice, Toggle, Slider, Page, Group };
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace roro::release {
|
||||
|
||||
// Installing a release without being asked to (issue #52): once a newer one is known, wait for a
|
||||
// quiet moment (nothing recording, nothing written, no key for a while), then say so on the
|
||||
// screen for 30 seconds. Unless someone answers no, it is installed. Host-tested; the caller says
|
||||
// what "quiet" is and does the installing.
|
||||
class AutoUpdate {
|
||||
public:
|
||||
static constexpr uint32_t kIdleMs = 120000; // no key for this long: nobody is using the device
|
||||
static constexpr uint32_t kAskMs = 30000; // how long the question stays before it answers itself
|
||||
|
||||
enum class Action : uint8_t {
|
||||
None,
|
||||
Ask, // the question goes up
|
||||
Dismiss, // the question comes down, nothing is installed
|
||||
Install, // the question comes down, install tag()
|
||||
};
|
||||
|
||||
// A newer release is known. A second offer while one is waiting or asked replaces its tag.
|
||||
void offer(const std::string& tag) {
|
||||
tag_ = tag;
|
||||
if (state_ == State::Idle) state_ = State::Waiting;
|
||||
}
|
||||
// The setting went off, or the release is no longer wanted.
|
||||
void withdraw() { withdrawn_ = true; }
|
||||
// The user's answer to the question.
|
||||
void answer(bool install) {
|
||||
if (state_ != State::Asking) return;
|
||||
answered_ = true;
|
||||
accepted_ = install;
|
||||
}
|
||||
|
||||
// Call often. `busy`: something is recording or being written, and a restart would cut it.
|
||||
// `idleMs`: how long since the last key.
|
||||
Action step(uint32_t nowMs, bool busy, uint32_t idleMs) {
|
||||
bool withdrawn = withdrawn_;
|
||||
withdrawn_ = false;
|
||||
switch (state_) {
|
||||
case State::Idle: return Action::None;
|
||||
case State::Waiting:
|
||||
if (withdrawn) return state_ = State::Idle, Action::None;
|
||||
if (busy || idleMs < kIdleMs) return Action::None;
|
||||
state_ = State::Asking;
|
||||
askedMs_ = nowMs;
|
||||
answered_ = false;
|
||||
return Action::Ask;
|
||||
case State::Asking:
|
||||
if (withdrawn) return state_ = State::Idle, Action::Dismiss;
|
||||
if (answered_) return state_ = State::Idle, accepted_ ? Action::Install : Action::Dismiss;
|
||||
// Something started meanwhile (a Track from a console): the question goes, and comes back later.
|
||||
if (busy) return state_ = State::Waiting, Action::Dismiss;
|
||||
if (nowMs - askedMs_ >= kAskMs) return state_ = State::Idle, Action::Install;
|
||||
return Action::None;
|
||||
}
|
||||
return Action::None;
|
||||
}
|
||||
|
||||
bool waiting() const { return state_ == State::Waiting; }
|
||||
bool asking() const { return state_ == State::Asking; }
|
||||
const std::string& tag() const { return tag_; }
|
||||
// Whole seconds until the question answers itself, rounded up.
|
||||
int secondsLeft(uint32_t nowMs) const {
|
||||
uint32_t gone = nowMs - askedMs_;
|
||||
return !asking() || gone >= kAskMs ? 0 : static_cast<int>((kAskMs - gone + 999) / 1000);
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State : uint8_t { Idle, Waiting, Asking };
|
||||
State state_ = State::Idle;
|
||||
std::string tag_;
|
||||
uint32_t askedMs_ = 0;
|
||||
bool answered_ = false, accepted_ = false, withdrawn_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro::release
|
||||
@@ -24,6 +24,12 @@ inline bool shouldAnnounce(const Release& latest, const std::string& running, co
|
||||
return isNewer(latest, running) && latest.tag != failed && latest.tag != announced;
|
||||
}
|
||||
|
||||
// Should the background check offer to install it by itself (issue #52)? Any newer release, each
|
||||
// day it is still the newest; never a version that rolled back on this device.
|
||||
inline bool shouldInstall(const Release& latest, const std::string& running, const std::string& failed) {
|
||||
return isNewer(latest, running) && latest.tag != failed;
|
||||
}
|
||||
|
||||
// Is `url` a download this device takes from the project's server, for this repository? Whatever
|
||||
// the API says, the device only fetches from where it asked (a redirected or rewritten answer
|
||||
// can't send it elsewhere).
|
||||
|
||||
@@ -56,6 +56,7 @@ const Definition kDefinitions[] = {
|
||||
{"launcher_list", Kind::Bool, 0, nullptr, 0, 1}, // off: the grid
|
||||
{"theme", Kind::Int, 0, nullptr, 0, 6}, // roro9stack's own; ui::kThemeCount of them
|
||||
{"theme_light", Kind::Bool, 0, nullptr, 0, 1},
|
||||
{"auto_install", Kind::Bool, 0, nullptr, 0, 1}, // off: what runs here changes only when someone says so
|
||||
};
|
||||
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||
"every Setting needs a definition");
|
||||
|
||||
@@ -44,6 +44,7 @@ enum class Setting : uint8_t {
|
||||
LauncherList, // bool: the Launcher is a list of names instead of a grid of icons (issue #9)
|
||||
Theme, // int: the colour theme, an index into ui::palette (issue #10)
|
||||
ThemeLight, // bool: its light side instead of its dark one
|
||||
InstallUpdates, // bool: a newer release found by the daily check is installed without being asked for (issue #52)
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ With no PC and no card, the device can install the project's releases itself (do
|
||||
- **Latest release** checks the server (Enter, or `c`) and says `v0.11.0 (new)` or `(current)`. Enter again opens the release: its version, date, size and the tag's message, with **Install** when it's newer. The download goes straight into the inactive slot, so no card is needed; the signature is checked after the first 160 bytes, before anything is written, and the image's hash at the end. The new firmware then runs on Probation as for any update.
|
||||
- **Older releases** lists the last ten, newest first. Opening an older one offers to go back to it, with a different question.
|
||||
- **Settings → System → Check for updates** (on by default): once a day, with Wi-Fi up and the clock set, the device looks at the latest release and says `v0.11.0 is out: see Settings > System > Firmware`, once per version. It installs nothing by itself, and doesn't announce a version that already failed and rolled back on this device.
|
||||
- **Settings → System → Install updates** (`When asked` by default; `By itself`, issue #52): a newer release found by the daily check is installed without being asked for. The device waits until no key was pressed for 2 minutes and nothing would be cut by a restart (a Track, a Capture, a copy, an upload, an SSH session, a web share), then asks on the screen for 30 seconds, lighting it if it was off: Cancel leaves it until the next day's check; Accept, or no answer, installs it and restarts. What was installed and when, and what was undone, is written in `/system/updates.log` on the card.
|
||||
|
||||
The connection is checked against the two ISRG roots Let's Encrypt chains end in (ADR 0009), not the usual bundle of about 130 authorities. Whatever the connection, the Update File's own signature is what decides what gets installed.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ docs = true
|
||||
source = "docs/milestones/R1.md"
|
||||
tag = "R1"
|
||||
+++
|
||||
**Status:** in progress. Shipped: CI and signed releases on Gitea (issue #5), a release for every tag; updates from Gitea (#6, **v0.11.0**); one firmware with the Debug Console in it (#68, **v0.12.0**); CI in about a minute (#74, **v0.13.0**). Not started: the Issues App (#4), automatic installs (#52), release channels (#53), resuming a download (#54).
|
||||
**Status:** in progress. Shipped: CI and signed releases on Gitea (issue #5), a release for every tag; updates from Gitea (#6, **v0.11.0**); one firmware with the Debug Console in it (#68, **v0.12.0**); CI in about a minute (#74, **v0.13.0**). Built, not released yet: installing a release by itself (#52). Not started: the Issues App (#4), release channels (#53), resuming a download (#54).
|
||||
|
||||
**Goal:** a tag is a release, built the same way every time and published where a device can find it.
|
||||
|
||||
@@ -132,6 +132,38 @@ The device looks at the project's Gitea for a newer release, says so, and instal
|
||||
|
||||
**Two slips during the checks:** a blind sequence of keys on the Firmware page opened the SD card's install dialog (the page keeps its selection between visits); it was cancelled with Back, nothing installed. And my port-polling while waiting for a restart took the Debug Console's only client slot, which made the first install attempt look like a failure.
|
||||
|
||||
## Installing a release by itself (issue #52)
|
||||
|
||||
For a device that sits on a desk, "always on the latest" may be what is wanted. Until now the daily check only said that a release was out.
|
||||
|
||||
### Decisions (2026-10-11)
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| Q275 | **A setting, off by default:** Settings > System > Install updates, `When asked` or `By itself`. It changes what runs on the device without anyone looking, so it is never on unless the owner said so. |
|
||||
| Q276 | **The owner is asked, and silence is yes:** a question over whatever is open, `Cancel` or `Accept`, that answers itself after 30 seconds by installing. The screen lights up for it if it was off. (The owner's own answer on the issue.) |
|
||||
| Q277 | **The question comes before the download,** not before the restart: the download already takes the screen over and may take IRC offline for its memory, so that is the moment to ask. |
|
||||
| Q278 | **Only in a quiet moment:** no key for 2 minutes, and nothing a restart would cut: a Track, a Capture, a file operation, an upload, an scp transfer, an SSH session, a web share, a noise test, another update. If one of them starts while the question is up, the question goes and comes back later with a full count. |
|
||||
| Q279 | **Any newer release,** not only patch releases, and with no delay after it is published: Probation and Rollback are what protect against a bad one, and a version that rolled back here is never offered again. |
|
||||
| Q280 | **Cancel lasts a day:** the release is offered again at the next daily check, for as long as it is the newest. |
|
||||
| Q281 | **A record:** each update that held (and whether it installed by itself), and each one that was undone, is a dated line in `/system/updates.log` on the card. |
|
||||
|
||||
### As built
|
||||
|
||||
- **`lib/release/src/auto_update.h`**, host-tested (10 tests): the waiting, the question and its count, as a state machine that is told what "busy" and "idle" are.
|
||||
- **The Update Service** offers the release at each background check when the setting is on (`shouldInstall`), and writes the record when Probation confirms a version or a rollback is reported.
|
||||
- **The main loop** decides what busy is, puts the question up through `Screen::render`, and gives it every key while it is up.
|
||||
- `update status` says whether a release is waiting for its quiet moment.
|
||||
|
||||
### Checked on the device (2026-10-11)
|
||||
|
||||
With the setting on, `update pretend v0.22.0` and `update daily`:
|
||||
|
||||
- The question came up over the Settings App, counting down each second. **Cancel** took it down and nothing was installed.
|
||||
- Left unanswered, the device downloaded v0.23.0, installed it and restarted into it.
|
||||
- `/system/updates.log` got its line at each confirmed update.
|
||||
- Not checked: "by itself" in the record (the release installed did not have this code yet), the 2 minutes without a key and the busy cases (host tests only), and a screen that was off lighting up. Keys sent through the Debug Console do not count as keys.
|
||||
|
||||
## One firmware: the Debug Console in every build (issue #68)
|
||||
|
||||
The issue asked for a token that could be set, so that Debug Builds could be published. The design round ended somewhere simpler: no Debug Builds. The console is in every firmware, off until switched on, with a token that belongs to the device (ADR 0010, which supersedes part of ADR 0004).
|
||||
|
||||
@@ -138,7 +138,7 @@ Settings had grown to 21 rows in one list. It now opens on five groups, each a s
|
||||
| Display | Brightness, Dim after, Screen off after, Theme, Light or dark, Launcher |
|
||||
| GNSS and radio | GNSS, Pause GNSS for LoRa, Coordinates, Probe MACs |
|
||||
| Network | Wi-Fi, VPN |
|
||||
| System | Check for updates, Firmware, Debug Console, About |
|
||||
| System | Check for updates, Install updates, Firmware, Debug Console, About |
|
||||
|
||||
- Enter opens a group, Back returns to the groups with the selection on the one just left, and Back from the groups leaves Settings. An open group has its name over its rows.
|
||||
- `SettingsMenu` (`lib/apps_model`) holds the groups and which one is open; every index is into what is listed. Host-tested: every setting is in exactly one group, and no group has more rows than fit under its name.
|
||||
|
||||
@@ -50,6 +50,7 @@ Inside a group, move with the arrows. On a toggle, a choice or a slider, left an
|
||||
| Setting | What it is |
|
||||
|---|---|
|
||||
| **Check for updates** | Once a day, see [Updates](/guide/updates/) |
|
||||
| **Install updates** | `When asked` (the default), or `By itself`: a newer release is installed in a quiet moment, after a question that answers itself in 30 seconds. See [Updates](/guide/updates/) |
|
||||
| **Firmware** | The page described in [Updates](/guide/updates/) |
|
||||
| **Debug Console** | Off unless you switch it on. It lets a PC on the same network read the device's console and drive it, with a token shown on this page: see [the developer docs](/dev/debug/switch-it-on/). Leave it off if that means nothing to you |
|
||||
| **About** | The firmware version, the node number, battery, memory, uptime, clock and licence |
|
||||
|
||||
@@ -21,7 +21,19 @@ An install needs Wi-Fi if it is a download. The new firmware is checked before a
|
||||
|
||||
## Check for updates
|
||||
|
||||
**Settings → System → Check for updates** is on by default. Once a day, with Wi-Fi up and the clock set, the device looks at the latest release and says `v0.11.0 is out: see Settings > System > Firmware`, once per version. It installs **nothing** by itself, and it does not announce a version that already failed and rolled back on this device.
|
||||
**Settings → System → Check for updates** is on by default. Once a day, with Wi-Fi up and the clock set, the device looks at the latest release and says `v0.11.0 is out: see Settings > System > Firmware`, once per version. It installs **nothing** by itself unless you ask for that (below), and it does not announce a version that already failed and rolled back on this device.
|
||||
|
||||
## Install updates by itself
|
||||
|
||||
**Settings → System → Install updates** is `When asked` by default. Set to `By itself`, a newer release found by the daily check is installed without your going to the Firmware page:
|
||||
|
||||
1. The device waits for a **quiet moment**: no key pressed for 2 minutes, and nothing a restart would cut short (a Track or a Capture recording, files being copied or received, an SSH session, a web share).
|
||||
2. It then asks on the screen, over whatever is open, and lights the screen if it was off: `Update to v0.24.0?`, with a count from **30 seconds**.
|
||||
3. **Cancel** (or <kbd>`</kbd>) leaves things as they are until the next day's check. **Accept**, or no answer when the count ends, downloads the release, installs it and restarts.
|
||||
|
||||
Everything else is as for an install you ask for: the signature is checked before anything is written, the new firmware runs on Probation, and a version that rolled back on this device is not offered again. It needs **Check for updates** to be on.
|
||||
|
||||
Each update that held, whoever started it, and each one that was undone, is written with its date in `/system/updates.log` on the SD card.
|
||||
|
||||
## Probation and Rollback
|
||||
|
||||
|
||||
+69
-3
@@ -20,6 +20,7 @@
|
||||
#include "services/web_share.h"
|
||||
#include "apps/gemini_app.h"
|
||||
#include "apps/gnss_app.h"
|
||||
#include "auto_update.h"
|
||||
#include "apps/irc_app.h"
|
||||
#include "apps/launcher_app.h"
|
||||
#include "apps/lora_scanner_app.h"
|
||||
@@ -81,6 +82,12 @@ static Screen screen;
|
||||
static BatteryService* battery;
|
||||
static StorageService* storageService;
|
||||
static FileOps* fileOps;
|
||||
static WebShare* webShare;
|
||||
// Installing a release by itself (issue #52): see autoUpdateStep().
|
||||
static release::AutoUpdate autoUpdate;
|
||||
static std::unique_ptr<DialogModel> autoDialog;
|
||||
static uint32_t lastKeyMs = 0;
|
||||
static bool autoRedraw = false;
|
||||
static std::vector<std::string> filesInUse(const std::string& path, bool folder);
|
||||
static PowerService* power;
|
||||
static ClockService* clockService;
|
||||
@@ -238,7 +245,7 @@ void setup() {
|
||||
apps->registerApp({"gnss", "GNSS", false, new GnssApp(*gnssService, settings, *storageService), icons::gnss});
|
||||
apps->registerApp({"gemini", "Gemini", false, new GeminiApp(*geminiService), icons::gemini});
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService), icons::lora});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *new WebShare(*storageService, *fileOps, *wifi)), icons::storage});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *(webShare = new WebShare(*storageService, *fileOps, *wifi))), icons::storage});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power), icons::notes});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
@@ -569,6 +576,9 @@ static void updateCommand(const String& args) {
|
||||
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: installing by itself %s%s%s\n", settings.getBool(Setting::InstallUpdates) ? "on" : "off",
|
||||
autoUpdate.waiting() ? ", waiting for a quiet moment to offer " : autoUpdate.asking() ? ", asking about " : "",
|
||||
autoUpdate.waiting() || autoUpdate.asking() ? autoUpdate.tag().c_str() : "");
|
||||
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 ")) {
|
||||
@@ -849,8 +859,57 @@ static void screenshotKey() {
|
||||
}
|
||||
|
||||
// Every key goes through here, from the keyboard or from a console's `key`.
|
||||
// Installing a release by itself (issue #52, Settings > System > Install updates): once the daily
|
||||
// check has found one, wait until nothing would be cut by a restart and no key was pressed for a
|
||||
// while, then ask on the screen, over whatever is open. Left unanswered for 30 s, it is installed.
|
||||
|
||||
// Would a restart now cut something short?
|
||||
static bool busyForRestart() {
|
||||
return gnssService->tracking() || loraCapture->capturing() || upload.active() || fileOps->busy() ||
|
||||
(scpService && scpService->busy()) || (sshService && sshService->active()) || (webShare && webShare->running()) ||
|
||||
(noiseTest && noiseTest->running()) || update->phase() != UpdateService::Phase::Idle || update->onProbation() ||
|
||||
update->giteaBusy();
|
||||
}
|
||||
|
||||
static void autoUpdateStep(uint32_t now) {
|
||||
static int shownSeconds = -1;
|
||||
if (!settings.getBool(Setting::InstallUpdates) && (autoUpdate.waiting() || autoUpdate.asking())) autoUpdate.withdraw();
|
||||
std::string tag;
|
||||
if (update->takeOffer(tag)) autoUpdate.offer(tag);
|
||||
if (autoDialog && autoDialog->result() != DialogModel::kPending) autoUpdate.answer(autoDialog->result() == 1);
|
||||
switch (autoUpdate.step(now, busyForRestart(), now - lastKeyMs)) {
|
||||
case release::AutoUpdate::Action::None: break;
|
||||
case release::AutoUpdate::Action::Ask:
|
||||
autoDialog.reset(new DialogModel({"Cancel", "Accept"}));
|
||||
power->onNotification(now, now + release::AutoUpdate::kAskMs); // a screen that is off lights up for it
|
||||
autoRedraw = true;
|
||||
break;
|
||||
case release::AutoUpdate::Action::Dismiss:
|
||||
autoDialog.reset();
|
||||
autoRedraw = true;
|
||||
break;
|
||||
case release::AutoUpdate::Action::Install: {
|
||||
autoDialog.reset();
|
||||
autoRedraw = true;
|
||||
std::string why = update->requestInstall(autoUpdate.tag(), true);
|
||||
if (!why.empty())
|
||||
bus.publish(Event::withText(EventType::Notification, ("Update not installed: " + why).c_str(), static_cast<int32_t>(NotificationLevel::Warning)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
int seconds = autoUpdate.asking() ? autoUpdate.secondsLeft(now) : -1;
|
||||
if (seconds != shownSeconds) {
|
||||
shownSeconds = seconds;
|
||||
autoRedraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void handleKey(const KeyEvent& e) {
|
||||
if (e.key == Key::Screenshot) return screenshotKey();
|
||||
if (autoDialog) { // the question takes every key until it is answered
|
||||
autoDialog->onKey(e);
|
||||
return;
|
||||
}
|
||||
apps->handleKey(e);
|
||||
}
|
||||
|
||||
@@ -1534,6 +1593,7 @@ static void loopPass() {
|
||||
printListingWhenReady();
|
||||
M5Cardputer.update();
|
||||
if (M5Cardputer.Keyboard.isChange()) {
|
||||
lastKeyMs = now;
|
||||
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen
|
||||
keyMapper.setTextEntry(apps->foreground().textEntryActive());
|
||||
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
|
||||
@@ -1549,6 +1609,7 @@ static void loopPass() {
|
||||
bus.dispatch();
|
||||
notifier->update(now);
|
||||
apps->update(now);
|
||||
autoUpdateStep(now);
|
||||
|
||||
if (now - lastLog > 10000) {
|
||||
lastLog = now;
|
||||
@@ -1594,10 +1655,15 @@ static void loopPass() {
|
||||
screen.invalidate();
|
||||
redrawAfterUpdate = true;
|
||||
}
|
||||
if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate) {
|
||||
if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate || autoRedraw) {
|
||||
lastStatus = status;
|
||||
redrawAfterUpdate = false;
|
||||
screen.render(*apps, status, toast);
|
||||
autoRedraw = false;
|
||||
if (autoDialog) {
|
||||
Screen::Question question{"Update to " + autoUpdate.tag() + "?",
|
||||
"It installs in " + std::to_string(autoUpdate.secondsLeft(now)) + " s unless you cancel, and the device restarts.", autoDialog.get()};
|
||||
screen.render(*apps, status, toast, &question);
|
||||
} else screen.render(*apps, status, toast);
|
||||
update->firstFrameDrawn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,26 @@ void UpdateService::notify(const std::string& text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(level)));
|
||||
}
|
||||
|
||||
// A line in the record of updates on the card (issue #52): the firmware can change without
|
||||
// anyone looking, so what was installed and when is written down.
|
||||
void UpdateService::record(const std::string& what) {
|
||||
time_t now = time(nullptr);
|
||||
char when[24] = "undated";
|
||||
if (now >= kClockSetAfter) {
|
||||
struct tm t;
|
||||
gmtime_r(&now, &t);
|
||||
strftime(when, sizeof when, "%Y-%m-%d %H:%M UTC", &t);
|
||||
}
|
||||
storage_.appendLine(kRecordPath, std::string(when) + " " + what);
|
||||
}
|
||||
|
||||
bool UpdateService::takeOffer(std::string& tag) {
|
||||
if (!offer_) return false;
|
||||
offer_ = false;
|
||||
tag = offered_;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string UpdateService::incomingVersion() const { return incoming_; }
|
||||
|
||||
void UpdateService::start() {
|
||||
@@ -108,6 +128,8 @@ void UpdateService::start() {
|
||||
store_.getString("ota_from", from);
|
||||
if (!pending.empty() && pending != versionString()) {
|
||||
notify("Update to " + pending + " failed, back on " + versionString(), NotificationLevel::Warning);
|
||||
record(pending + " failed and was undone: back on " + versionString());
|
||||
store_.putString("ota_auto", "");
|
||||
store_.putString("ota_failed", pending); // not announced again until there's a newer one (Q168)
|
||||
store_.putString("ota_pending", "");
|
||||
}
|
||||
@@ -142,6 +164,14 @@ void UpdateService::tick(uint32_t nowMs) {
|
||||
store_.putString("ota_pending", "");
|
||||
store_.putInt("ota_attempts", 0);
|
||||
notify(std::string("Updated to ") + versionString(), NotificationLevel::Info);
|
||||
{
|
||||
std::string from, automatic;
|
||||
store_.getString("ota_from", from);
|
||||
store_.getString("ota_auto", automatic);
|
||||
record(std::string(versionString()) + " installed" + (automatic == versionString() ? " by itself" : "") +
|
||||
(from.empty() ? "" : ", from " + from));
|
||||
store_.putString("ota_auto", "");
|
||||
}
|
||||
break;
|
||||
case Probation::Verdict::RollBack:
|
||||
// ota_pending still names this version: the previous firmware will report the failure.
|
||||
@@ -215,12 +245,13 @@ std::string UpdateService::failedVersion() const {
|
||||
return failed;
|
||||
}
|
||||
|
||||
std::string UpdateService::requestInstall(const std::string& tag) {
|
||||
std::string UpdateService::requestInstall(const std::string& tag, bool automatic) {
|
||||
if (phase_ != Phase::Idle || giteaBusy()) return "Busy with something else";
|
||||
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";
|
||||
store_.putString("ota_auto", automatic ? tag : "");
|
||||
installTag_ = tag;
|
||||
request_ = Request::Install;
|
||||
return "";
|
||||
@@ -281,7 +312,14 @@ void UpdateService::serve() {
|
||||
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) &&
|
||||
// Installing by itself (issue #52): offered at each daily check while it is the newest,
|
||||
// so a "not now" lasts a day. The main loop asks on the screen when the moment is quiet.
|
||||
if (r == Request::BackgroundCheck && gitea_.latest(latest) && settings_.getBool(Setting::InstallUpdates) &&
|
||||
release::shouldInstall(latest, runningVersion(), failedVersion())) {
|
||||
announced_ = latest.tag;
|
||||
offered_ = latest.tag;
|
||||
offer_ = true;
|
||||
} else if (r == Request::BackgroundCheck && gitea_.latest(latest) &&
|
||||
release::shouldAnnounce(latest, runningVersion(), failedVersion(), announced_)) {
|
||||
announced_ = latest.tag;
|
||||
notify(latest.tag + " is out: Settings > System > Firmware", NotificationLevel::Info); // 48 bytes at most
|
||||
|
||||
@@ -51,7 +51,12 @@ class UpdateService : public Service {
|
||||
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.
|
||||
std::string requestInstall(const std::string& tag);
|
||||
// `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.
|
||||
@@ -85,6 +90,7 @@ class UpdateService : public Service {
|
||||
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_;
|
||||
@@ -103,7 +109,8 @@ class UpdateService : public Service {
|
||||
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_;
|
||||
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_;
|
||||
|
||||
+3
-2
@@ -12,12 +12,12 @@ bool Screen::begin() {
|
||||
return canvas_.createSprite(theme::kWidth, theme::kHeight) != nullptr;
|
||||
}
|
||||
|
||||
void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toast) {
|
||||
void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toast, const Question* question) {
|
||||
const auto& area = theme::kContent;
|
||||
App& app = apps.foreground();
|
||||
// An App that keeps what it drew: only the Status Bar is cleared, unless the App has changed
|
||||
// or a Toast or the help panel, drawn over it, has just gone.
|
||||
bool overlay = apps.help().isOpen() || toast, retains = app.retainsContent();
|
||||
bool overlay = apps.help().isOpen() || toast || question, retains = app.retainsContent();
|
||||
if (overlayWas_ && !overlay) lost_ = true;
|
||||
if (retains && &app == lastApp_ && !lost_) {
|
||||
canvas_.fillRect(0, 0, theme::kWidth, area.y, theme::kBackground);
|
||||
@@ -31,6 +31,7 @@ void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toa
|
||||
canvas_.setClipRect(area.x, area.y, area.w, area.h);
|
||||
app.draw(canvas_);
|
||||
if (apps.help().isOpen()) widgets::help(canvas_, apps.help()); // over the App, under the Status Bar
|
||||
if (question) widgets::dialog(canvas_, question->title, question->message, *question->dialog);
|
||||
canvas_.clearClipRect();
|
||||
widgets::statusBar(canvas_, status);
|
||||
if (toast) widgets::toast(canvas_, *toast);
|
||||
|
||||
+6
-1
@@ -10,7 +10,12 @@ namespace roro {
|
||||
class Screen {
|
||||
public:
|
||||
bool begin(); // allocates the frame buffer (~32 KB at 8-bit colour)
|
||||
void render(AppManager& apps, const StatusInfo& status, const Toast* toast);
|
||||
// A question the device itself asks, over whatever App is open (issue #52).
|
||||
struct Question {
|
||||
std::string title, message;
|
||||
const DialogModel* dialog;
|
||||
};
|
||||
void render(AppManager& apps, const StatusInfo& status, const Toast* toast, const Question* question = nullptr);
|
||||
// Takes over the screen while a Firmware Update is received or about to restart.
|
||||
void renderUpdate(const std::string& title, const std::string& detail, int percent);
|
||||
Canvas& canvas() { return canvas_; }
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "auto_update.h"
|
||||
#include "update_check.h"
|
||||
|
||||
using namespace roro::release;
|
||||
using Action = AutoUpdate::Action;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kIdle = AutoUpdate::kIdleMs, kAsk = AutoUpdate::kAskMs;
|
||||
Release release(const char* tag) {
|
||||
Release r;
|
||||
r.tag = tag;
|
||||
r.otaUrl = "https://git.twis.la/twisla/roro9stack/releases/download/x/x.ota";
|
||||
r.otaSize = 1;
|
||||
return r;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void test_nothing_happens_without_an_offer() {
|
||||
AutoUpdate a;
|
||||
TEST_ASSERT_TRUE(a.step(1000, false, kIdle) == Action::None);
|
||||
TEST_ASSERT_FALSE(a.waiting());
|
||||
TEST_ASSERT_FALSE(a.asking());
|
||||
}
|
||||
|
||||
void test_it_waits_for_a_quiet_moment() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
TEST_ASSERT_TRUE(a.waiting());
|
||||
TEST_ASSERT_TRUE(a.step(1000, false, kIdle - 1) == Action::None); // a key not long ago
|
||||
TEST_ASSERT_TRUE(a.step(2000, true, kIdle * 10) == Action::None); // a Track is recording
|
||||
TEST_ASSERT_TRUE(a.step(3000, false, kIdle) == Action::Ask);
|
||||
TEST_ASSERT_TRUE(a.asking());
|
||||
TEST_ASSERT_EQUAL_STRING("v1.2.0", a.tag().c_str());
|
||||
}
|
||||
|
||||
void test_unanswered_it_installs_after_30_seconds() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.step(5000, false, kIdle);
|
||||
TEST_ASSERT_EQUAL(30, a.secondsLeft(5000));
|
||||
TEST_ASSERT_EQUAL(30, a.secondsLeft(5001));
|
||||
TEST_ASSERT_EQUAL(1, a.secondsLeft(5000 + kAsk - 1));
|
||||
TEST_ASSERT_TRUE(a.step(5000 + kAsk - 1, false, kIdle) == Action::None);
|
||||
TEST_ASSERT_TRUE(a.step(5000 + kAsk, false, kIdle) == Action::Install);
|
||||
TEST_ASSERT_FALSE(a.asking());
|
||||
TEST_ASSERT_TRUE(a.step(5000 + kAsk + 1, false, kIdle) == Action::None); // once
|
||||
}
|
||||
|
||||
void test_accept_installs_at_once() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.step(0, false, kIdle);
|
||||
a.answer(true);
|
||||
TEST_ASSERT_TRUE(a.step(100, false, 0) == Action::Install);
|
||||
}
|
||||
|
||||
void test_cancel_drops_it_until_the_next_offer() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.step(0, false, kIdle);
|
||||
a.answer(false);
|
||||
TEST_ASSERT_TRUE(a.step(100, false, 0) == Action::Dismiss);
|
||||
TEST_ASSERT_TRUE(a.step(kAsk * 2, false, kIdle * 2) == Action::None);
|
||||
a.offer("v1.2.0"); // the next day's check
|
||||
TEST_ASSERT_TRUE(a.step(kAsk * 3, false, kIdle) == Action::Ask);
|
||||
}
|
||||
|
||||
void test_something_starting_takes_the_question_down() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.step(0, false, kIdle);
|
||||
TEST_ASSERT_TRUE(a.step(1000, true, kIdle) == Action::Dismiss);
|
||||
TEST_ASSERT_TRUE(a.waiting());
|
||||
TEST_ASSERT_TRUE(a.step(2000, true, kIdle) == Action::None);
|
||||
TEST_ASSERT_TRUE(a.step(90000, false, kIdle) == Action::Ask); // and it comes back, with a full count
|
||||
TEST_ASSERT_EQUAL(30, a.secondsLeft(90000));
|
||||
}
|
||||
|
||||
void test_the_setting_going_off_withdraws_it() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.withdraw();
|
||||
TEST_ASSERT_TRUE(a.step(0, false, kIdle) == Action::None);
|
||||
TEST_ASSERT_FALSE(a.waiting());
|
||||
a.offer("v1.2.0");
|
||||
a.step(0, false, kIdle);
|
||||
a.withdraw();
|
||||
TEST_ASSERT_TRUE(a.step(10, false, kIdle) == Action::Dismiss);
|
||||
TEST_ASSERT_FALSE(a.asking());
|
||||
}
|
||||
|
||||
void test_an_answer_is_only_for_a_question() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.answer(true); // nothing was asked yet
|
||||
TEST_ASSERT_TRUE(a.step(0, false, kIdle) == Action::Ask);
|
||||
TEST_ASSERT_TRUE(a.step(1, false, kIdle) == Action::None);
|
||||
}
|
||||
|
||||
void test_a_newer_offer_replaces_the_tag() {
|
||||
AutoUpdate a;
|
||||
a.offer("v1.2.0");
|
||||
a.offer("v1.3.0");
|
||||
a.step(0, false, kIdle);
|
||||
TEST_ASSERT_EQUAL_STRING("v1.3.0", a.tag().c_str());
|
||||
}
|
||||
|
||||
void test_what_is_offered() {
|
||||
// Newer than what runs, and not a version that rolled back here.
|
||||
TEST_ASSERT_TRUE(shouldInstall(release("v1.2.0"), "v1.1.0", ""));
|
||||
TEST_ASSERT_FALSE(shouldInstall(release("v1.1.0"), "v1.1.0", ""));
|
||||
TEST_ASSERT_FALSE(shouldInstall(release("v1.0.0"), "v1.1.0", ""));
|
||||
TEST_ASSERT_FALSE(shouldInstall(release("v1.2.0"), "v1.1.0", "v1.2.0"));
|
||||
Release noFile = release("v1.2.0");
|
||||
noFile.otaUrl.clear();
|
||||
TEST_ASSERT_FALSE(shouldInstall(noFile, "v1.1.0", ""));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_nothing_happens_without_an_offer);
|
||||
RUN_TEST(test_it_waits_for_a_quiet_moment);
|
||||
RUN_TEST(test_unanswered_it_installs_after_30_seconds);
|
||||
RUN_TEST(test_accept_installs_at_once);
|
||||
RUN_TEST(test_cancel_drops_it_until_the_next_offer);
|
||||
RUN_TEST(test_something_starting_takes_the_question_down);
|
||||
RUN_TEST(test_the_setting_going_off_withdraws_it);
|
||||
RUN_TEST(test_an_answer_is_only_for_a_question);
|
||||
RUN_TEST(test_a_newer_offer_replaces_the_tag);
|
||||
RUN_TEST(test_what_is_offered);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -67,7 +67,7 @@ void test_every_setting_is_in_one_group() {
|
||||
total += f.menu.count();
|
||||
f.menu.close();
|
||||
}
|
||||
TEST_ASSERT_EQUAL(21, total);
|
||||
TEST_ASSERT_EQUAL(22, total);
|
||||
int i = -1;
|
||||
TEST_ASSERT_TRUE(f.menu.reveal(SettingsMenu::Row::Vpn, i));
|
||||
TEST_ASSERT_EQUAL_STRING("Network", f.menu.title().c_str());
|
||||
@@ -204,6 +204,16 @@ void test_pause_gnss_for_lora_is_off_by_default() {
|
||||
|
||||
// R1, Q165: the device looks for a newer release once a day. It installs nothing by itself, so it
|
||||
// is on unless switched off.
|
||||
void test_installing_updates_by_itself_is_off_by_default() {
|
||||
Fixture f;
|
||||
int row = f.row(SettingsMenu::Row::InstallUpdates);
|
||||
TEST_ASSERT_EQUAL_STRING("Install updates", f.menu.label(row).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("When asked", f.menu.value(row).c_str());
|
||||
f.menu.toggle(row);
|
||||
TEST_ASSERT_TRUE(f.settings.getBool(Setting::InstallUpdates));
|
||||
TEST_ASSERT_EQUAL_STRING("By itself", f.menu.value(row).c_str());
|
||||
}
|
||||
|
||||
void test_check_for_updates_is_on_by_default() {
|
||||
Fixture f;
|
||||
int row = f.row(SettingsMenu::Row::CheckUpdates);
|
||||
@@ -245,6 +255,7 @@ int main() {
|
||||
RUN_TEST(test_names_are_text_rows_with_their_byte_limits);
|
||||
RUN_TEST(test_pause_gnss_for_lora_is_off_by_default);
|
||||
RUN_TEST(test_check_for_updates_is_on_by_default);
|
||||
RUN_TEST(test_installing_updates_by_itself_is_off_by_default);
|
||||
RUN_TEST(test_gnss_and_coordinate_rows_toggle);
|
||||
RUN_TEST(test_the_debug_console_is_a_page_that_says_off);
|
||||
return UNITY_END();
|
||||
|
||||
Reference in New Issue
Block a user