#pragma once #include #include #include /** * @brief Non-template core of PersistableStore. * * All ArduinoJson parse/serialize machinery is instantiated once here (in * PersistableStore.cpp) instead of in every store's translation unit. GCC * emits the JSON serializer/parser templates as local .isra clones per TU * (~0.5KB each), so keeping serializeJson/deserializeJson out of the stores * is what makes the abstraction flash-neutral. */ class PersistableStoreBase { protected: PersistableStoreBase() = default; ~PersistableStoreBase() = default; // Serializes doc and writes it to path (ensures /.crosspoint exists). Logs on failure. static bool writeDocToFile(const char* path, const JsonDocument& doc); // Reads path and parses it into doc. Returns false silently when the file // does not exist (expected on first boot); logs on read/parse failure. static bool readDocFromFile(const char* path, JsonDocument& doc); /** * Helper function for extracting an obfuscated password from a JSON value. * Accepts JsonVariantConst so callers can pass either a whole JsonDocument * or a JsonObject element (e.g. inside an array iteration). * If the decoded password requires a resave (e.g. from plaintext fallback), `needsResave` is set to true. */ static std::string extractPassword(JsonVariantConst doc, bool& needsResave); }; /** * @brief Base class for persistable singletons using CRTP. * * Derived classes must provide: * - A private default constructor * - friend class PersistableStore; * - static const char* getFilePath(); * - void toJson(JsonDocument& doc) const; * - bool fromJson(JsonVariantConst doc); * * Note for implementers: read string values as `const char*` (e.g. * `obj["name"] | ""`), never as `| std::string("")` — ArduinoJson's * std::string converter drags a per-TU copy of the whole JSON serializer * into flash via its serializeJson fallback. */ template class PersistableStore : public PersistableStoreBase { protected: PersistableStore() = default; ~PersistableStore() = default; public: // Delete copy constructor and assignment PersistableStore(const PersistableStore&) = delete; PersistableStore& operator=(const PersistableStore&) = delete; static T& getInstance() { static T instance; return instance; } bool saveToFile() const { JsonDocument doc; static_cast(this)->toJson(doc); return writeDocToFile(T::getFilePath(), doc); } bool loadFromFile() { JsonDocument doc; if (!readDocFromFile(T::getFilePath(), doc)) { return false; } return static_cast(this)->fromJson(doc.as()); } };