Files
roro9stack/lib/services/src/settings.cpp
T
twislaandClaude Opus 5.5 7fe8b3d22a
CI / build (pull_request) Successful in 7m22s
Site / build (pull_request) Successful in 14s
Help: Fn+h lists the keys of the screen you're on, and no screen names its keys any more (#69)
Fn+h on any screen, text fields included, and ? outside Text Entry, open a
panel over the content area: the screen's own keys, then the ones that work
everywhere. Every App declares its keys for the state it is in (pages,
viewers, dialogs and text fields answer for themselves); the App manager
opens the panel and takes every key while it is open.

About 30 hint lines are gone, from every App. What stays on a screen is
state. The first-start Setup keeps its hints and teaches the key; a device
set up before gets one Toast, once. The guide and the FAQ open with it.
`key help` over the consoles.

476 host tests (8 new). Checked on the device with key help and screenshots:
the Launcher, all nine Apps and several of their states. 8.5 KB of flash and
40 bytes of static RAM. Decisions Q196 to Q203 in docs/milestones/U1.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-07 01:29:51 +02:00

142 lines
5.2 KiB
C++

#include "settings.h"
#include "debug_auth.h"
#include "ipv4.h"
namespace roro {
namespace {
enum class Kind : uint8_t { Bool, Int, String };
struct Definition {
const char* key;
Kind kind;
int32_t defaultInt;
const char* defaultString;
int32_t min;
int32_t max; // for strings: max length in bytes
};
// Order must match the Setting enum.
const Definition kDefinitions[] = {
{"setup_done", Kind::Bool, 0, nullptr, 0, 1},
{"long_name", Kind::String, 0, "", 1, 39},
{"short_name", Kind::String, 0, "", 1, 4},
{"region", Kind::String, 0, "EU868", 1, 8},
{"region_ok", Kind::Bool, 0, nullptr, 0, 1},
{"timezone", Kind::String, 0, "CET-1CEST,M3.5.0,M10.5.0/3", 1, 63},
{"brightness", Kind::Int, 60, nullptr, 10, 100},
{"dim_s", Kind::Int, 30, nullptr, 5, 600},
{"off_s", Kind::Int, 60, nullptr, 10, 3600},
{"sound", Kind::Bool, 1, nullptr, 0, 1},
{"probe_mac_raw", Kind::Bool, 1, nullptr, 0, 1},
{"wifi_on", Kind::Bool, 1, nullptr, 0, 1},
{"gnss_on", Kind::Bool, 1, nullptr, 0, 1},
{"coord_dms", Kind::Bool, 0, nullptr, 0, 1},
{"lora_preset", Kind::Int, 0, nullptr, 0, 6}, // LongFast first
{"dns1", Kind::String, 0, "9.9.9.9", 7, 15}, // Quad9
{"dns2", Kind::String, 0, "1.1.1.1", 0, 15}, // Cloudflare
{"dns_always", Kind::Bool, 0, nullptr, 0, 1},
{"ntp1", Kind::String, 0, "pool.ntp.org", 1, 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)
{"check_updates", Kind::Bool, 1, nullptr, 0, 1}, // on: it only looks, and says so (R1, Q165)
{"debug_on", Kind::Bool, 0, nullptr, 0, 1}, // off: nothing listens until the owner says so (Q189)
{"debug_token", Kind::String, 0, "", 0, 64}, // empty, or a valid token
{"help_told", Kind::Bool, 0, nullptr, 0, 1},
};
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
"every Setting needs a definition");
const char* const kKnownRegions[] = {"EU868"};
const Definition& def(Setting s) { return kDefinitions[static_cast<size_t>(s)]; }
} // namespace
Settings::Settings(KeyValueStore& store, EventBus& bus) : store_(store), bus_(bus) {}
const char* Settings::key(Setting s) { return def(s).key; }
void Settings::load() {
for (size_t i = 0; i < values_.size(); i++) {
auto s = static_cast<Setting>(i);
const auto& d = def(s);
auto& v = values_[i];
if (d.kind == Kind::String) {
v.str = d.defaultString;
std::string stored;
if (store_.getString(d.key, stored) && validString(s, stored)) v.str = stored;
} else {
v.i = d.defaultInt;
int32_t stored;
if (store_.getInt(d.key, stored) && validInt(s, stored)) v.i = stored;
}
}
}
int32_t Settings::getInt(Setting s) const { return values_[static_cast<size_t>(s)].i; }
bool Settings::getBool(Setting s) const { return getInt(s) != 0; }
const std::string& Settings::getString(Setting s) const { return values_[static_cast<size_t>(s)].str; }
bool Settings::validInt(Setting s, int32_t value) const {
const auto& d = def(s);
if (d.kind == Kind::String || value < d.min || value > d.max) return false;
if (s == Setting::DimTimeoutS) return value < getInt(Setting::OffTimeoutS);
if (s == Setting::OffTimeoutS) return value > getInt(Setting::DimTimeoutS);
return true;
}
bool Settings::validString(Setting s, const std::string& value) const {
const auto& d = def(s);
if (d.kind != Kind::String) return false;
if (value.size() < static_cast<size_t>(d.min) || value.size() > static_cast<size_t>(d.max)) return false;
if (s == Setting::Region) {
for (auto region : kKnownRegions)
if (value == region) return true;
return false;
}
uint32_t address;
if (s == Setting::Dns1) return net::parseIpv4(value, address);
if (s == Setting::Dns2) return value.empty() || net::parseIpv4(value, address);
if (s == Setting::Ntp1) return net::validHost(value);
if (s == Setting::Ntp2) return value.empty() || net::validHost(value);
if (s == Setting::DebugToken) return value.empty() || debug::validToken(value);
return true;
}
bool Settings::setInt(Setting s, int32_t value) {
if (def(s).kind == Kind::Bool) return false;
if (!validInt(s, value)) return false;
if (getInt(s) == value) return true;
values_[static_cast<size_t>(s)].i = value;
store_.putInt(key(s), value);
changed(s);
return true;
}
bool Settings::setBool(Setting s, bool value) {
if (def(s).kind != Kind::Bool) return false;
if (getBool(s) == value) return true;
values_[static_cast<size_t>(s)].i = value ? 1 : 0;
store_.putInt(key(s), value ? 1 : 0);
changed(s);
return true;
}
bool Settings::setString(Setting s, const std::string& value) {
if (!validString(s, value)) return false;
if (getString(s) == value) return true;
values_[static_cast<size_t>(s)].str = value;
store_.putString(key(s), value);
changed(s);
return true;
}
void Settings::changed(Setting s) {
bus_.publish(Event::withText(EventType::SettingChanged, key(s), static_cast<int32_t>(s)));
}
} // namespace roro