Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

793 lines
19 KiB

/*
SETTINGS MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
*/
#include "espurna.h"
#include "crash.h"
#include "terminal.h"
#include "storage_eeprom.h"
#include <algorithm>
#include <vector>
#include <cstdlib>
#include <ArduinoJson.h>
// -----------------------------------------------------------------------------
namespace espurna {
namespace settings {
namespace {
// Depending on features enabled, we may end up with different left boundary
// Settings are written right-to-left, so we only have issues when there are a lot of key-values
// XXX: slightly hacky, because we EEPROMr.length() is 0 before we enter setup() code
static kvs_type kv_store(
EepromStorage{},
#if DEBUG_SUPPORT
EepromReservedSize + crashReservedSize(),
#else
EepromReservedSize,
#endif
EepromSize
);
} // namespace
namespace query {
const Setting* Setting::findFrom(const Setting* begin, const Setting* end, StringView key) {
for (auto it = begin; it != end; ++it) {
if ((*it) == key) {
return it;
}
}
return end;
}
String Setting::findValueFrom(const Setting* begin, const Setting* end, StringView key) {
String out;
const auto value = findFrom(begin, end, key);
if (value != end) {
out = (*value).value();
}
return out;
}
bool IndexedSetting::findSamePrefix(const IndexedSetting* begin, const IndexedSetting* end, StringView key) {
for (auto it = begin; it != end; ++it) {
if (samePrefix(key, (*it).prefix())) {
return true;
}
}
return false;
}
String IndexedSetting::findValueFrom(Iota iota, const IndexedSetting* begin, const IndexedSetting* end, StringView key) {
String out;
while (iota) {
for (auto it = begin; it != end; ++it) {
const auto expected = Key(
(*it).prefix().toString(), *iota);
if (key == expected.value()) {
out = (*it).value(*iota);
goto output;
}
}
++iota;
}
output:
return out;
}
namespace internal {
namespace {
std::forward_list<Handler> handlers;
} // namespace
} // namespace internal
String find(StringView key) {
String out;
for (const auto& handler : internal::handlers) {
if (handler.check(key)) {
out = handler.get(key);
break;
}
}
return out;
}
} // namespace query
namespace options {
bool EnumerationNumericHelper::check(const String& value) {
if (value.length()) {
if ((value.length() > 1) && (*value.begin() == '0')) {
return false;
}
for (auto it = value.begin(); it != value.end(); ++it) {
switch (*it) {
case '0'...'9':
break;
default:
return false;
}
}
return true;
}
return false;
}
} // namespace options
ValueResult get(const String& key) {
return kv_store.get(key);
}
bool set(const String& key, const String& value) {
return kv_store.set(key, value);
}
bool del(const String& key) {
return kv_store.del(key);
}
bool has(const String& key) {
return kv_store.has(key);
}
Keys keys() {
Keys out;
kv_store.foreach([&](kvs_type::KeyValueResult&& kv) {
out.push_back(kv.key.read());
});
return out;
}
size_t available() {
return kv_store.available();
}
size_t size() {
return kv_store.size();
}
void foreach(KeyValueResultCallback&& callback) {
kv_store.foreach(callback);
}
void foreach_prefix(PrefixResultCallback&& callback, query::StringViewIterator prefixes) {
kv_store.foreach([&](kvs_type::KeyValueResult&& kv) {
auto key = kv.key.read();
for (auto it = prefixes.begin(); it != prefixes.end(); ++it) {
if (query::samePrefix(StringView{key}, (*it))) {
callback((*it), std::move(key), kv.value);
}
}
});
}
// --------------------------------------------------------------------------
namespace internal {
template <>
float convert(const String& value) {
return atof(value.c_str());
}
template <>
double convert(const String& value) {
return atof(value.c_str());
}
template <>
signed char convert(const String& value) {
return value.toInt();
}
template <>
short convert(const String& value) {
return value.toInt();
}
template <>
int convert(const String& value) {
return value.toInt();
}
template <>
long convert(const String& value) {
return value.toInt();
}
template <>
bool convert(const String& value) {
if (value.length()) {
if ((value == "0")
|| (value == "n")
|| (value == "no")
|| (value == "false")
|| (value == "off")) {
return false;
}
return (value == "1")
|| (value == "y")
|| (value == "yes")
|| (value == "true")
|| (value == "on");
}
return false;
}
template <>
uint32_t convert(const String& value) {
return parseUnsigned(value).value;
}
String serialize(uint32_t value, int base) {
return formatUnsigned(value, base);
}
template <>
unsigned long convert(const String& value) {
return convert<unsigned int>(value);
}
template <>
unsigned short convert(const String& value) {
return convert<unsigned long>(value);
}
template <>
unsigned char convert(const String& value) {
return convert<unsigned long>(value);
}
} // namespace internal
// TODO: UI needs this to avoid showing keys in storage order
std::vector<String> sorted_keys() {
auto values = keys();
std::sort(values.begin(), values.end(),
[](const String& lhs, const String& rhs) -> bool {
return rhs.compareTo(lhs) > 0;
});
return values;
}
#if TERMINAL_SUPPORT
namespace terminal {
namespace {
void dump(const ::terminal::CommandContext& ctx, const query::Setting* begin, const query::Setting* end) {
for (auto it = begin; it != end; ++it) {
ctx.output.printf_P(PSTR("> %s => %s\n"),
(*it).key().c_str(), (*it).value().c_str());
}
}
void dump(const ::terminal::CommandContext& ctx, const query::IndexedSetting* begin, const query::IndexedSetting* end, size_t index) {
for (auto it = begin; it != end; ++it) {
ctx.output.printf_P(PSTR("> %s%u => %s\n"),
(*it).prefix().c_str(), index,
(*it).value(index).c_str());
}
}
namespace commands {
PROGMEM_STRING(Config, "CONFIG");
void config(::terminal::CommandContext&& ctx) {
DynamicJsonBuffer jsonBuffer(1024);
JsonObject& root = jsonBuffer.createObject();
settingsGetJson(root);
root.prettyPrintTo(ctx.output);
terminalOK(ctx);
}
PROGMEM_STRING(Keys, "KEYS");
void keys(::terminal::CommandContext&& ctx) {
const auto keys = settings::sorted_keys();
String value;
for (const auto& key : keys) {
value = getSetting(key);
ctx.output.printf_P(PSTR("> %s => \"%s\"\n"),
key.c_str(), value.c_str());
}
const auto size = settings::size();
if (size > 0) {
const auto available = settings::available();
ctx.output.printf_P(PSTR("Number of keys: %u\n"), keys.size());
ctx.output.printf_P(PSTR("Available: %u bytes (%u%%)\n"),
available, (100 * available) / size);
}
terminalOK(ctx);
}
PROGMEM_STRING(Gc, "GC");
void gc(::terminal::CommandContext&& ctx) {
struct KeyRef {
String key;
size_t length;
};
using KeyRefs = std::vector<KeyRef>;
KeyRefs refs;
kv_store.foreach([&](kvs_type::KeyValueResult&& result) {
refs.push_back(
KeyRef{
.key = result.key.read(),
.length = result.key.length(),
});
});
auto is_ascii = [](const String& value) -> bool {
for (const auto& c : value) {
if (!isascii(c)) {
return false;
}
}
return true;
};
std::vector<const String*> broken;
for (const auto& ref : refs) {
if ((ref.length != ref.key.length()) || !is_ascii(ref.key)) {
broken.push_back(&ref.key);
}
}
size_t count = 0;
for (const auto& key : broken) {
settings::del(*key);
++count;
}
ctx.output.printf_P("deleted %zu keys\n", count);
terminalOK(ctx);
}
PROGMEM_STRING(Del, "DEL");
void del(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("del <key> [<key>...]"));
return;
}
int result = 0;
for (auto it = (ctx.argv.begin() + 1); it != ctx.argv.end(); ++it) {
result += settings::del(*it);
}
if (result) {
terminalOK(ctx);
} else {
terminalError(ctx, F("no keys were removed"));
}
}
PROGMEM_STRING(Set, "SET");
void set(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() != 3) {
terminalError(ctx, F("set <key> <value>"));
return;
}
if (settings::set(ctx.argv[1], ctx.argv[2])) {
terminalOK(ctx);
return;
}
terminalError(ctx, F("could not set the key"));
}
PROGMEM_STRING(Get, "GET");
void get(::terminal::CommandContext&& ctx) {
if (ctx.argv.size() < 2) {
terminalError(ctx, F("get <key> [<key>...]"));
return;
}
for (auto it = (ctx.argv.cbegin() + 1); it != ctx.argv.cend(); ++it) {
auto result = settings::get(*it);
if (!result) {
const auto maybeValue = query::find(*it);
if (maybeValue.length()) {
ctx.output.printf_P(PSTR("> %s => %s (default)\n"),
(*it).c_str(), maybeValue.c_str());
} else {
ctx.output.printf_P(PSTR("> %s =>\n"), (*it).c_str());
}
continue;
}
ctx.output.printf_P(PSTR("> %s => \"%s\"\n"), (*it).c_str(), result.c_str());
}
terminalOK(ctx);
}
PROGMEM_STRING(Reload, "RELOAD");
void reload(::terminal::CommandContext&& ctx) {
espurnaReload();
terminalOK(ctx);
}
PROGMEM_STRING(FactoryReset, "FACTORY.RESET");
void factory_reset(::terminal::CommandContext&& ctx) {
factoryReset();
terminalOK(ctx);
}
[[gnu::unused]]
PROGMEM_STRING(Save, "SAVE");
[[gnu::unused]]
void save(::terminal::CommandContext&& ctx) {
eepromCommit();
terminalOK(ctx);
}
static constexpr ::terminal::Command List[] PROGMEM {
{Config, commands::config},
{Keys, commands::keys},
{Gc, commands::gc},
{Del, commands::del},
{Set, commands::set},
{Get, commands::get},
{Reload, commands::reload},
{FactoryReset, commands::factory_reset},
#if not SETTINGS_AUTOSAVE
{Save, commands::save},
#endif
};
} // namespace commands
void setup() {
espurna::terminal::add(commands::List);
}
} // namespace
} // namespace terminal
#endif
} // namespace settings
} // namespace espurna
// -----------------------------------------------------------------------------
// Key-value API
// -----------------------------------------------------------------------------
size_t settingsSize() {
return espurna::settings::size() - espurna::settings::available();
}
espurna::settings::Keys settingsKeys() {
return espurna::settings::sorted_keys();
}
void settingsRegisterQueryHandler(espurna::settings::query::Handler handler) {
espurna::settings::query::internal::handlers.push_front(handler);
}
String settingsQuery(espurna::StringView key) {
return espurna::settings::query::find(key);
}
void moveSetting(const String& from, const String& to) {
const auto result = espurna::settings::get(from);
if (result) {
setSetting(to, result.ref());
delSetting(from);
}
}
struct SettingsKeyPair {
espurna::settings::Key from;
espurna::settings::Key to;
};
void moveSetting(const String& from, const String& to, size_t index) {
const auto keys = SettingsKeyPair{
.from = {from, index},
.to = {to, index}
};
const auto result = espurna::settings::get(keys.from.value());
if (result) {
setSetting(keys.to, result.ref());
delSetting(keys.from);
}
}
void moveSettings(const String& from, const String& to) {
for (size_t index = 0; index < 100; ++index) {
const auto keys = SettingsKeyPair{
.from = {from, index},
.to = {to, index},
};
const auto result = espurna::settings::get(keys.from.value());
if (!result) {
break;
}
setSetting(keys.to, result.ref());
delSetting(keys.from);
}
}
template
bool getSetting(const espurna::settings::Key& key, bool defaultValue);
template
int getSetting(const espurna::settings::Key& key, int defaultValue);
template
long getSetting(const espurna::settings::Key& key, long defaultValue);
template
unsigned char getSetting(const espurna::settings::Key& key, unsigned char defaultValue);
template
unsigned short getSetting(const espurna::settings::Key& key, unsigned short defaultValue);
template
unsigned int getSetting(const espurna::settings::Key& key, unsigned int defaultValue);
template
unsigned long getSetting(const espurna::settings::Key& key, unsigned long defaultValue);
template
float getSetting(const espurna::settings::Key& key, float defaultValue);
template
double getSetting(const espurna::settings::Key& key, double defaultValue);
String getSetting(const String& key) {
return std::move(espurna::settings::get(key)).get();
}
String getSetting(const __FlashStringHelper* key) {
return getSetting(espurna::settings::Key(key));
}
String getSetting(const char* key) {
return getSetting(espurna::settings::Key(key));
}
String getSetting(const espurna::settings::Key& key) {
return getSetting(key, espurna::StringView(""));
}
String getSetting(const espurna::settings::Key& key, const char* defaultValue) {
return getSetting(key, espurna::StringView(defaultValue));
}
String getSetting(const espurna::settings::Key& key, const __FlashStringHelper* defaultValue) {
return getSetting(key, espurna::StringView(defaultValue));
}
String getSetting(const espurna::settings::Key& key, const String& defaultValue) {
auto result = espurna::settings::get(key.value());
if (result) {
return std::move(result).get();
}
return defaultValue;
}
String getSetting(const espurna::settings::Key& key, String&& defaultValue) {
String out;
auto result = espurna::settings::get(key.value());
if (result) {
out = std::move(result).get();
} else {
out = std::move(defaultValue);
}
return out;
}
String getSetting(const espurna::settings::Key& key, espurna::StringView defaultValue) {
String out;
auto result = espurna::settings::get(key.value());
if (result) {
out = std::move(result).get();
} else {
out = defaultValue.toString();
}
return out;
}
bool delSetting(const String& key) {
return espurna::settings::del(key);
}
bool delSetting(const espurna::settings::Key& key) {
return delSetting(key.value());
}
bool delSetting(const char* key) {
return delSetting(String(key));
}
bool delSetting(const __FlashStringHelper* key) {
return delSetting(String(key));
}
bool hasSetting(const String& key) {
return espurna::settings::has(key);
}
bool hasSetting(const espurna::settings::Key& key) {
return hasSetting(key.value());
}
bool hasSetting(const char* key) {
return hasSetting(String(key));
}
bool hasSetting(const __FlashStringHelper* key) {
return hasSetting(String(key));
}
void saveSettings() {
#if not SETTINGS_AUTOSAVE
eepromCommit();
#endif
}
void autosaveSettings() {
#if SETTINGS_AUTOSAVE
eepromCommit();
#endif
}
void resetSettings() {
eepromClear();
}
// -----------------------------------------------------------------------------
// API
// -----------------------------------------------------------------------------
bool settingsRestoreJson(JsonObject& data) {
// Note: we try to match what /config generates, expect {"app":"ESPURNA",...}
const auto& app = data[F("app")];
if (!app.success() || !app.is<const char*>()) {
DEBUG_MSG_P(PSTR("[SETTING] Missing 'app' key\n"));
return false;
}
const auto* data_app = app.as<const char*>();
const auto build_app = buildApp().name;
if (build_app != data_app) {
DEBUG_MSG_P(PSTR("[SETTING] Invalid 'app' key\n"));
return false;
}
// .../config will add this key, but it is optional
if (data[F("backup")].as<bool>()) {
resetSettings();
}
// These three are just metadata, no need to actually store them
for (auto element : data) {
auto key = String(element.key);
if (key.startsWith(F("app"))
|| key.startsWith(F("version"))
|| key.startsWith(F("backup")))
{
continue;
}
setSetting(std::move(key), String(element.value.as<String>()));
}
saveSettings();
DEBUG_MSG_P(PSTR("[SETTINGS] Settings restored successfully\n"));
return true;
}
bool settingsRestoreJson(char* json_string, size_t json_buffer_size) {
// XXX: as of right now, arduinojson cannot trigger callbacks for each key individually
// Manually separating kv pairs can allow to parse only a small chunk, since we know that there is only string type used (even with bools / ints). Can be problematic when parsing data that was not generated by us.
// Current parsing method is limited only by keys (~sizeof(uintptr_t) bytes per key, data is not copied when string is non-const)
DynamicJsonBuffer jsonBuffer(json_buffer_size);
JsonObject& root = jsonBuffer.parseObject((char *) json_string);
if (!root.success()) {
DEBUG_MSG_P(PSTR("[SETTINGS] JSON parsing error\n"));
return false;
}
return settingsRestoreJson(root);
}
void settingsGetJson(JsonObject& root) {
auto keys = espurna::settings::sorted_keys();
for (const auto& key : keys) {
auto value = getSetting(key);
root[key] = value;
}
}
// -----------------------------------------------------------------------------
// Initialization
// -----------------------------------------------------------------------------
#if TERMINAL_SUPPORT
void settingsDump(const ::terminal::CommandContext& ctx,
const espurna::settings::query::Setting* begin,
const espurna::settings::query::Setting* end)
{
espurna::settings::terminal::dump(ctx, begin, end);
}
void settingsDump(const ::terminal::CommandContext& ctx,
const espurna::settings::query::IndexedSetting* begin,
const espurna::settings::query::IndexedSetting* end, size_t index)
{
espurna::settings::terminal::dump(ctx, begin, end, index);
}
namespace {
} // namespace
#endif
void settingsSetup() {
#if TERMINAL_SUPPORT
espurna::settings::terminal::setup();
#endif
}