Files
espnav-display/src/ConfigManager.cpp
T
2026-06-18 15:04:18 +02:00

261 lines
8.6 KiB
C++

/**
* @file ConfigManager.cpp
* @brief Implementation of persistent configuration management via LittleFS.
*/
#include "ConfigManager.h"
#include <LittleFS.h>
#include <ArduinoJson.h>
// Singleton instance
ConfigManager configManager;
// =============================================================================
// Public API
// =============================================================================
bool ConfigManager::begin() {
if (!LittleFS.begin(true /* format on fail */)) {
Serial.println("[Config] ERROR: LittleFS mount failed");
return false;
}
Serial.println("[Config] LittleFS mounted");
if (!_load()) {
Serial.println("[Config] No valid config found — applying defaults");
_applyDefaults();
_write();
}
return true;
}
bool ConfigManager::save(const AppConfig& cfg) {
_cfg = cfg;
return _write();
}
bool ConfigManager::persist() {
return _write();
}
bool ConfigManager::setActivePage(uint8_t index) {
if (index >= _cfg.pageCount) return false;
_cfg.activePage = index;
return _write();
}
void ConfigManager::nextPage() {
_cfg.activePage = (_cfg.activePage + 1) % _cfg.pageCount;
_write();
}
void ConfigManager::prevPage() {
_cfg.activePage = (_cfg.activePage == 0)
? _cfg.pageCount - 1
: _cfg.activePage - 1;
_write();
}
bool ConfigManager::setTile(uint8_t page, uint8_t tile, DataField field, const char* label) {
if (page >= MAX_PAGES || tile >= Display::TILE_COUNT) return false;
_cfg.pages[page].tiles[tile].field = field;
if (label && strlen(label) < sizeof(TileConfig::label)) {
strlcpy(_cfg.pages[page].tiles[tile].label, label, sizeof(TileConfig::label));
}
return _write();
}
void ConfigManager::toJson(String& out) const {
// Use a JsonDocument sized for the whole config
JsonDocument doc;
doc["activePage"] = _cfg.activePage;
doc["pageCount"] = _cfg.pageCount;
doc["apSSID"] = _cfg.apSSID;
// Do not serialise AP password to the web UI for security
JsonArray pages = doc["pages"].to<JsonArray>();
for (uint8_t p = 0; p < _cfg.pageCount; p++) {
JsonObject page = pages.add<JsonObject>();
page["name"] = _cfg.pages[p].name;
JsonArray tiles = page["tiles"].to<JsonArray>();
for (uint8_t t = 0; t < Display::TILE_COUNT; t++) {
JsonObject tile = tiles.add<JsonObject>();
tile["field"] = static_cast<uint8_t>(_cfg.pages[p].tiles[t].field);
tile["fieldName"] = fieldName(_cfg.pages[p].tiles[t].field);
tile["label"] = _cfg.pages[p].tiles[t].label;
}
}
// Append available fields list so the web UI can build dropdowns
JsonArray fields = doc["availableFields"].to<JsonArray>();
for (uint8_t i = 0; i < static_cast<uint8_t>(DataField::_COUNT); i++) {
JsonObject f = fields.add<JsonObject>();
f["id"] = i;
f["name"] = fieldName(static_cast<DataField>(i));
f["unit"] = fieldUnit(static_cast<DataField>(i));
}
serializeJson(doc, out);
}
bool ConfigManager::fromJson(const String& json) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, json);
if (err) {
Serial.printf("[Config] JSON parse error: %s\n", err.c_str());
return false;
}
if (doc.containsKey("apSSID")) {
strlcpy(_cfg.apSSID, doc["apSSID"] | _cfg.apSSID, sizeof(_cfg.apSSID));
}
if (doc.containsKey("apPassword")) {
strlcpy(_cfg.apPassword, doc["apPassword"] | _cfg.apPassword, sizeof(_cfg.apPassword));
}
JsonArray pages = doc["pages"].as<JsonArray>();
if (pages) {
uint8_t pCount = 0;
for (JsonObject page : pages) {
if (pCount >= MAX_PAGES) break;
strlcpy(_cfg.pages[pCount].name, page["name"] | "", sizeof(PageConfig::name));
uint8_t tIdx = 0;
for (JsonObject tile : page["tiles"].as<JsonArray>()) {
if (tIdx >= Display::TILE_COUNT) break;
uint8_t fid = tile["field"] | 0;
if (fid < static_cast<uint8_t>(DataField::_COUNT)) {
_cfg.pages[pCount].tiles[tIdx].field = static_cast<DataField>(fid);
}
strlcpy(_cfg.pages[pCount].tiles[tIdx].label,
tile["label"] | "",
sizeof(TileConfig::label));
tIdx++;
}
pCount++;
}
if (pCount > 0) _cfg.pageCount = pCount;
}
if (doc.containsKey("activePage")) {
uint8_t ap = doc["activePage"];
if (ap < _cfg.pageCount) _cfg.activePage = ap;
}
return _write();
}
void ConfigManager::resetToDefaults() {
_applyDefaults();
_write();
}
// =============================================================================
// Private helpers
// =============================================================================
bool ConfigManager::_load() {
if (!LittleFS.exists(CONFIG_PATH)) return false;
File f = LittleFS.open(CONFIG_PATH, "r");
if (!f) return false;
String json = f.readString();
f.close();
// Re-use fromJson but do not re-persist to avoid re-entry
JsonDocument doc;
if (deserializeJson(doc, json) != DeserializationError::Ok) return false;
// --- activePage / pageCount ---
_cfg.activePage = doc["activePage"] | 0;
_cfg.pageCount = doc["pageCount"] | 1;
if (_cfg.pageCount == 0 || _cfg.pageCount > MAX_PAGES) _cfg.pageCount = 1;
if (_cfg.activePage >= _cfg.pageCount) _cfg.activePage = 0;
strlcpy(_cfg.apSSID, doc["apSSID"] | "MarineDisplay", sizeof(_cfg.apSSID));
strlcpy(_cfg.apPassword, doc["apPassword"] | "marine123", sizeof(_cfg.apPassword));
JsonArray pages = doc["pages"].as<JsonArray>();
uint8_t pIdx = 0;
for (JsonObject page : pages) {
if (pIdx >= MAX_PAGES) break;
strlcpy(_cfg.pages[pIdx].name, page["name"] | "", sizeof(PageConfig::name));
uint8_t tIdx = 0;
for (JsonObject tile : page["tiles"].as<JsonArray>()) {
if (tIdx >= Display::TILE_COUNT) break;
uint8_t fid = tile["field"] | 0;
_cfg.pages[pIdx].tiles[tIdx].field =
(fid < static_cast<uint8_t>(DataField::_COUNT))
? static_cast<DataField>(fid)
: DataField::NONE;
strlcpy(_cfg.pages[pIdx].tiles[tIdx].label,
tile["label"] | "",
sizeof(TileConfig::label));
tIdx++;
}
pIdx++;
}
Serial.printf("[Config] Loaded %u page(s), active=%u\n", _cfg.pageCount, _cfg.activePage);
return true;
}
bool ConfigManager::_write() {
File f = LittleFS.open(CONFIG_PATH, "w");
if (!f) {
Serial.println("[Config] ERROR: Cannot open config file for writing");
return false;
}
JsonDocument doc;
doc["activePage"] = _cfg.activePage;
doc["pageCount"] = _cfg.pageCount;
doc["apSSID"] = _cfg.apSSID;
doc["apPassword"] = _cfg.apPassword;
JsonArray pages = doc["pages"].to<JsonArray>();
for (uint8_t p = 0; p < _cfg.pageCount; p++) {
JsonObject page = pages.add<JsonObject>();
page["name"] = _cfg.pages[p].name;
JsonArray tiles = page["tiles"].to<JsonArray>();
for (uint8_t t = 0; t < Display::TILE_COUNT; t++) {
JsonObject tile = tiles.add<JsonObject>();
tile["field"] = static_cast<uint8_t>(_cfg.pages[p].tiles[t].field);
tile["label"] = _cfg.pages[p].tiles[t].label;
}
}
size_t written = serializeJson(doc, f);
f.close();
Serial.printf("[Config] Saved %u bytes to %s\n", written, CONFIG_PATH);
return written > 0;
}
void ConfigManager::_applyDefaults() {
_cfg = AppConfig{}; // zero-init
// Page 0 — Essential sailing data
_cfg.pageCount = 2;
_cfg.activePage = 0;
strlcpy(_cfg.pages[0].name, "Sailing", sizeof(PageConfig::name));
_cfg.pages[0].tiles[0] = { DataField::SOG, "SOG" };
_cfg.pages[0].tiles[1] = { DataField::TWA, "TWA" };
_cfg.pages[0].tiles[2] = { DataField::TWS, "TWS" };
_cfg.pages[0].tiles[3] = { DataField::VMG, "VMG" };
// Page 1 — Performance
strlcpy(_cfg.pages[1].name, "Performance", sizeof(PageConfig::name));
_cfg.pages[1].tiles[0] = { DataField::POLAR_PCT, "Polar %" };
_cfg.pages[1].tiles[1] = { DataField::TARGET_STW, "Tgt STW" };
_cfg.pages[1].tiles[2] = { DataField::AWA, "AWA" };
_cfg.pages[1].tiles[3] = { DataField::DEPTH, "Depth" };
strlcpy(_cfg.apSSID, "MarineDisplay", sizeof(_cfg.apSSID));
strlcpy(_cfg.apPassword, "marine123", sizeof(_cfg.apPassword));
}