Updates: when a release may install by itself, as a state machine

Issue #52: once a newer release is known, wait for a quiet moment, ask
for 30 seconds, and install unless the answer is no. The caller says
what busy and idle are. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-11 17:49:09 +02:00
co-authored by Claude Opus 5.5
parent 5af695f43a
commit 17651cb911
3 changed files with 223 additions and 0 deletions
+80
View File
@@ -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