init commit
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* @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));
|
||||
}
|
||||
Reference in New Issue
Block a user