firmware-base/docs/nc-persistence.md

7.5 KiB

Persistence Mixin for NetworkComponent

This document proposes a Persistence mixin for NetworkComponent to enable automatic saving and loading of NetworkValue states to/from disk.

1. Overview

The Persistence mixin adds functionality to any NetworkComponent derivative to persist its NetworkValues. This is useful for retaining state across reboots, such as configuration, calibration data, or last known operational parameters.

The functionality is modeled after the Settings component but is designed to be generic and reusable.

2. Design

The persistence logic will be encapsulated in a template class Persistent, which inherits from the NetworkComponent it extends. This allows for compile-time addition of the persistence feature without modifying the core NetworkComponent for components that do not require it.

To support serialization, we propose adding virtual methods to NetworkValueBase for JSON serialization.

2.1. Changes to NetworkValue.h

NetworkValueBase will be augmented with virtual methods for type identification and JSON handling.

// In NetworkValueBase
enum class NetworkValueType {
  NV_NONE,
  NV_BOOL,
  NV_UINT32,
  NV_INT32,
  NV_FLOAT,
  NV_STRING
  // ... other types
};

class NetworkValueBase {
public:
    // ... existing members
    virtual void toJSON(JsonObject& obj) const = 0;
    virtual void fromJSON(JsonObjectConst& obj) = 0;
    virtual NetworkValueType getType() const = 0;
    virtual const char* getName() const = 0;
};

The templated NetworkValue<T> class will implement these virtual methods.

// In NetworkValue<T>

template<typename T>
class NetworkValue : public NetworkValueBase {
public:
    // ... existing members

    void toJSON(JsonObject& obj) const override {
        obj["name"] = this->name.c_str();
        obj["value"] = m_value;
        obj["type"] = static_cast<int>(getType());
    }

    void fromJSON(JsonObjectConst& obj) override {
        if (obj["value"].is<T>()) {
            update(obj["value"].as<T>());
        }
    }

    const char* getName() const override {
        return name.c_str();
    }

    NetworkValueType getType() const override {
        // Specialize this for each supported type
        if (std::is_same<T, bool>::value) return NetworkValueType::NV_BOOL;
        if (std::is_same<T, uint32_t>::value) return NetworkValueType::NV_UINT32;
        // ...
        return NetworkValueType::NV_NONE;
    }
};

2.2. The Persistent Mixin

The mixin will manage a filename and provide load() and save() methods.

#include "NetworkComponent.h"
#include <LittleFS.h>
#include <ArduinoJson.h>

template <class TBase>
class Persistent : public TBase {
protected:
    String _persistenceFile;

public:
    template <typename... Args>
    Persistent(const char* persistenceFile, Args&&... args)
        : TBase(std::forward<Args>(args)...),
          _persistenceFile(persistenceFile) {}

    short setup() override {
        TBase::setup();
        load(); // Automatically load on setup
        return E_OK;
    }

    bool save() {
        if (!LittleFS.begin()) {
            Log.errorln(F("Failed to mount LittleFS for saving %s"), this->name.c_str());
            return false;
        }

        JsonDocument doc;
        JsonArray nvArray = doc.to<JsonArray>();

        for (const auto* nv : this->_networkValues) {
            if (nv) {
                nv->toJSON(nvArray.add<JsonObject>());
            }
        }

        File file = LittleFS.open(_persistenceFile, "w");
        if (!file) {
            Log.errorln(F("Failed to open file for writing: %s"), _persistenceFile.c_str());
            LittleFS.end();
            return false;
        }

        size_t bytesWritten = serializeJson(doc, file);
        file.close();
        LittleFS.end();

        return bytesWritten > 0;
    }

    bool load() {
        if (!LittleFS.begin(true)) {
            Log.errorln(F("Failed to mount LittleFS for loading %s"), this->name.c_str());
            return false;
        }

        if (!LittleFS.exists(_persistenceFile)) {
            Log.infoln(F("Persistence file not found: %s. Using defaults."), _persistenceFile.c_str());
            LittleFS.end();
            return false;
        }

        File file = LittleFS.open(_persistenceFile, "r");
        if (!file) {
             Log.errorln(F("Failed to open file for reading: %s"), _persistenceFile.c_str());
             LittleFS.end();
             return false;
        }

        JsonDocument doc;
        DeserializationError error = deserializeJson(doc, file);
        file.close();

        if (error) {
            Log.errorln(F("Failed to parse JSON from %s: %s"), _persistenceFile.c_str(), error.c_str());
            LittleFS.end();
            return false;
        }

        JsonArrayConst nvArray = doc.as<JsonArrayConst>();
        for (JsonObjectConst nvJson : nvArray) {
            const char* name = nvJson["name"];
            if (!name) continue;

            for (auto* nv : this->_networkValues) {
                if (nv && strcmp(nv->getName(), name) == 0) {
                    nv->fromJSON(nvJson);
                    break;
                }
            }
        }
        LittleFS.end();
        return true;
    }
};

3. Usage Example

Here's how a component MyComponent would use the Persistent mixin.

3.1. Component Definition

Instead of inheriting from NetworkComponent directly, it inherits from Persistent<NetworkComponent<...>>.

// MyComponent.h

#include "modbus/NetworkComponent.h"
#include "mixins/Persistent.h"

const size_t MYCOMPONENT_BLOCK_COUNT = 10;

class MyComponent : public Persistent<NetworkComponent<MYCOMPONENT_BLOCK_COUNT>> {
private:
    NetworkValue<float> _calibrationFactor;
    NetworkValue<int> _threshold;

public:
    MyComponent(Component* owner);
    short setup() override;
};

3.2. Component Implementation

The constructor passes the persistence filename and other NetworkComponent arguments up to the Persistent mixin constructor.

// MyComponent.cpp

#include "MyComponent.h"

MyComponent::MyComponent(Component* owner)
    : Persistent(
        "/mycomponent.json",
        MB_ADDR_MYCOMPONENT_BASE,
        "MyComponent",
        "my_comp_key",
        owner),
      _calibrationFactor(this, this->id, "CalibrationFactor", 1.0f),
      _threshold(this, this->id, "Threshold", 100)
{
    addNetworkValue(&_calibrationFactor);
    addNetworkValue(&_threshold);
}

short MyComponent::setup() {
    Persistent::setup();

    _calibrationFactor.initModbus(...);
    registerBlock(_calibrationFactor.getRegisterInfo());
    _threshold.initModbus(...);
    registerBlock(_threshold.getRegisterInfo());

    return E_OK;
}

void MyComponent::updateAndSaveChanges() {
    _calibrationFactor.update(1.23f);
    if (!save()) {
        Log.errorln(F("Failed to save MyComponent settings!"));
    }
}

4. JSON File Structure

The persisted file (e.g., /mycomponent.json) would look like this:

[
  {
    "name": "Enabled",
    "value": true,
    "type": 1
  },
  {
    "name": "CalibrationFactor",
    "value": 1.23,
    "type": 4
  },
  {
    "name": "Threshold",
    "value": 100,
    "type": 3
  }
]

This structure is a flat array of all NetworkValue objects managed by the component. Each object contains its name, value, and a type identifier.