forked from tbnobody/OpenDTU
-
-
Notifications
You must be signed in to change notification settings - Fork 94
Save Runtime data in LittleFS #2262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
SW-Niko
wants to merge
9
commits into
hoylabs:development
Choose a base branch
from
SW-Niko:RuntimeData
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9e11925
Runtime: Base
SW-Niko fc84ebd
Runtime: Integration
SW-Niko 10c327f
Runtime: fix read() timing
SW-Niko 15de96f
Runtime: remove static bool from getWriteTrigger()
SW-Niko e2b03c4
Runtime: commit the new state only after a successful write
SW-Niko d54e11c
Runtime: move write() to onFirmwareUpdateFinish()
SW-Niko 2ce71c8
Runtime: move default value from .cpp to .h
SW-Niko 8982927
Runtime: use lock for protect _lastTrigger
SW-Niko 97ffc77
Runtime: enable write on demand, fix lock issue in write()
SW-Niko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| #pragma once | ||
|
|
||
| #include <ArduinoJson.h> | ||
| #include <TaskSchedulerDeclarations.h> | ||
| #include <mutex> | ||
| #include <atomic> | ||
|
|
||
|
|
||
| class RuntimeClass { | ||
| public: | ||
| explicit RuntimeClass(uint16_t version) : _writeVersion(version) {}; | ||
| ~RuntimeClass() = default; | ||
| RuntimeClass(const RuntimeClass&) = delete; | ||
| RuntimeClass& operator=(const RuntimeClass&) = delete; | ||
| RuntimeClass(RuntimeClass&&) = delete; | ||
| RuntimeClass& operator=(RuntimeClass&&) = delete; | ||
|
|
||
| void init(Scheduler& scheduler); | ||
| bool read(void); | ||
| bool write(uint16_t const freezeMinutes = 10); // do not write if last write operation was less than freezeMinutes ago | ||
|
|
||
| uint16_t getWriteCount(void) const; | ||
| time_t getWriteEpochTime(void) const; | ||
| bool getReadState(void) const { return _readOK; } | ||
| bool getWriteState(void) const { return _writeOK; } | ||
| String getWriteCountAndTimeString(void) const; | ||
|
|
||
| private: | ||
| void loop(void); | ||
| bool getWriteTrigger(void); | ||
|
|
||
| Task _loopTask; | ||
| std::atomic<bool> _readOK = false; // true if the last read operation was successful | ||
| std::atomic<bool> _writeOK = false; // true if the last write operation was successful | ||
| mutable std::mutex _mutex; // to protect the shared data below | ||
| bool _lastTrigger = false; // auxiliary value to prevent multiple triggering on the same day | ||
| uint16_t _writeVersion = 0; // shared data: version of the runtime data | ||
| uint16_t _writeCount = 0; // shared data: number of write operations | ||
| time_t _writeEpoch = 0; // shared data: epoch time when the data was written | ||
| }; | ||
|
|
||
| extern RuntimeClass RuntimeData; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| /* Runtime Data Management | ||
| * | ||
| * Read and write runtime data persistent on LittleFS | ||
| * - The data is stored in JSON format | ||
| * - The data is written during WebApp 'OTA firmware upgrade' and during Webapp 'Reboot' | ||
| * - For security reasons such as 'unexpected power cycles' or 'physical resets', data is also written once a day at 00:05 | ||
| * - The data will not be written if the last write operation was less than one hour ago. | ||
| * - Threadsave access to the data is provided by a mutex. | ||
| * | ||
| * Note: Runtime data must be added in the read() and write() methods. | ||
| * To avoid reenter deadlocks, do not call write() or read() from a locally locked mutex to save local data on demand! | ||
| * | ||
| * 2025.09.11 - 1.0 - first version | ||
| */ | ||
|
|
||
| #include <Utils.h> | ||
| #include <LittleFS.h> | ||
| #include <LogHelper.h> | ||
| #include "RuntimeData.h" | ||
|
|
||
|
|
||
| #undef TAG | ||
| static const char* TAG = "runtimedata"; | ||
| static const char* SUBTAG = "Handler"; | ||
|
|
||
|
|
||
| constexpr const char* RUNTIME_FILENAME = "/runtime.json"; // filename of the runtime data file | ||
| constexpr uint16_t RUNTIME_VERSION = 0; // version of the runtime data structure, prepared for future use | ||
|
|
||
|
|
||
| RuntimeClass RuntimeData {RUNTIME_VERSION}; // singleton instance | ||
|
|
||
|
|
||
| /* | ||
| * Init the runtime data loop task | ||
| */ | ||
| void RuntimeClass::init(Scheduler& scheduler) | ||
| { | ||
| scheduler.addTask(_loopTask); | ||
| _loopTask.setCallback(std::bind(&RuntimeClass::loop, this)); | ||
| _loopTask.setIterations(TASK_FOREVER); | ||
| _loopTask.setInterval(60 * 1000); // every minute | ||
| _loopTask.enable(); | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * The runtime data loop is called every minute | ||
| */ | ||
| void RuntimeClass::loop(void) | ||
| { | ||
| // check whether it is time to write the runtime data but do not write if last write operation was less than 60 min ago | ||
| if (getWriteTrigger()) { write(60); } | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Writes the runtime data to LittleFS file | ||
| * freezeMinutes: Minimum necessary time [minutes] between now and last write operation | ||
| */ | ||
| bool RuntimeClass::write(uint16_t const freezeMinutes) | ||
| { | ||
| auto cleanExit = [this](const bool writeOk, const char* text) -> bool { | ||
| if (writeOk) { | ||
| DTU_LOGI("%s", text); | ||
| } else { | ||
| DTU_LOGE("%s", text); | ||
| } | ||
| _writeOK = writeOk; | ||
| return writeOk; | ||
| }; | ||
|
|
||
| // we need the next local time before we can write the runtime data | ||
| time_t nextEpoch; | ||
| if (!Utils::getEpoch(&nextEpoch, 5)) { return cleanExit(false, "Local time not available, skipping write"); } | ||
| uint16_t nextCount; | ||
|
|
||
| { // prepare the next write count | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
|
|
||
| // we do not write more than once in a hour | ||
| if ((_writeEpoch != 0) && (difftime(nextEpoch, _writeEpoch) < 60 * freezeMinutes)) { | ||
| return cleanExit(false, "Time interval between 2 write operations too short, skipping write"); | ||
| } | ||
| nextCount = _writeCount + 1; | ||
| } // mutex is automatically released when lock goes out of this scope | ||
|
|
||
| JsonDocument doc; | ||
| JsonObject info = doc["info"].to<JsonObject>(); | ||
| info["version"] = RUNTIME_VERSION; | ||
| info["save_count"] = nextCount; | ||
| info["save_epoch"] = nextEpoch; | ||
|
|
||
| // Insert additional runtime data here and protect the shared data with a local mutex | ||
|
|
||
|
|
||
|
|
||
| if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { | ||
| return cleanExit(false, "JSON alloc fault, skipping write"); | ||
| } | ||
|
|
||
| File fRuntime = LittleFS.open(RUNTIME_FILENAME, "w"); | ||
| if (!fRuntime) { return cleanExit(false, "Failed to open file for writing"); } | ||
|
|
||
| if (serializeJson(doc, fRuntime) == 0) { | ||
| fRuntime.close(); | ||
| return cleanExit(false, "Failed to serialize to file"); | ||
| } | ||
|
|
||
| fRuntime.close(); | ||
|
|
||
| { // commit the new state only after a successful write | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| _writeVersion = RUNTIME_VERSION; | ||
| _writeEpoch = nextEpoch; | ||
| _writeCount = nextCount; | ||
| } // mutex is automatically released when lock goes out of this scope | ||
|
|
||
| return cleanExit(true, "Written to file"); | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Read the runtime data from LittleFS file | ||
| */ | ||
| bool RuntimeClass::read(void) | ||
| { | ||
| bool readOk = false; | ||
| JsonDocument doc; | ||
|
|
||
| // Note: We do not exit on read or allocation errors. In that case we need the default values | ||
| File fRuntime = LittleFS.open(RUNTIME_FILENAME, "r", false); | ||
| if (fRuntime) { | ||
| Utils::skipBom(fRuntime); | ||
| DeserializationError error = deserializeJson(doc, fRuntime); | ||
| if (!error && Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { | ||
| readOk = true; // success of reading the runtime data | ||
| } | ||
| } | ||
|
|
||
| JsonObject info = doc["info"]; | ||
| { // mutex is automatically released when lock goes out of this scope | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| _writeVersion = info["version"] | RUNTIME_VERSION; | ||
| _writeCount = info["save_count"] | 0U; | ||
| _writeEpoch = info["save_epoch"] | 0U; | ||
| } // mutex is automatically released when lock goes out of this scope | ||
|
|
||
| // deserialize additional runtime data here, prepare default values and protect the shared data with a local mutex | ||
|
|
||
|
|
||
| if (fRuntime) { fRuntime.close(); } | ||
| if (readOk) { | ||
| DTU_LOGI("Read successfully"); | ||
| } else { | ||
| DTU_LOGE("Read fault, using default values"); | ||
| } | ||
| _readOK = readOk; | ||
| return readOk; | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Get the write counter | ||
| */ | ||
| uint16_t RuntimeClass::getWriteCount(void) const | ||
| { | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| return _writeCount; | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Get the write epoch time | ||
| */ | ||
| time_t RuntimeClass::getWriteEpochTime(void) const | ||
| { | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| return _writeEpoch; | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Get the write count and time as string | ||
| * Format: "<count> / <dd>-<mon> <hh>:<mm>" | ||
| * If epoch time and local time is not available the time is replaced by "no time" | ||
| */ | ||
| String RuntimeClass::getWriteCountAndTimeString(void) const | ||
| { | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| char buf[32] = ""; | ||
| struct tm time; | ||
|
|
||
| // Before we can convert the epoch to local time, we need to ensure we've received the correct time | ||
| // from the time server. This may take some time after the system boots. | ||
| if ((_writeEpoch != 0) && (getLocalTime(&time, 5))) { | ||
| localtime_r(&_writeEpoch, &time); | ||
| strftime(buf, sizeof(buf), " / %d-%h %R", &time); | ||
| } else { | ||
| snprintf(buf, sizeof(buf), " / no time"); | ||
| } | ||
| String ctString = String(_writeCount) + String(buf); | ||
| return ctString; | ||
| } | ||
|
|
||
|
|
||
| /* | ||
| * Returns true at the daily trigger time at 00:05 | ||
| */ | ||
| bool RuntimeClass::getWriteTrigger(void) { | ||
| std::lock_guard<std::mutex> lock(_mutex); | ||
| struct tm actTime; | ||
| if (getLocalTime(&actTime, 5)) { | ||
| if ((actTime.tm_hour == 0) && (actTime.tm_min >= 5) && (actTime.tm_min <= 10)) { | ||
| if (_lastTrigger == false) { | ||
| _lastTrigger = true; | ||
| return true; | ||
| } | ||
| } else { | ||
| _lastTrigger = false; | ||
| } | ||
| } | ||
| return false; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.