-
-
Notifications
You must be signed in to change notification settings - Fork 93
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
base: development
Are you sure you want to change the base?
Conversation
Build ArtifactsFirmware built from this pull request's code:
Notice
|
|
Runtime data also written to LittleFS if WebUI 'Device Reboot' is used. |
WalkthroughAdds a thread-safe RuntimeClass (RuntimeData) that persists runtime metadata to LittleFS as JSON with scheduled minute loop, read/write APIs and accessors; integrates RuntimeData.write() into OTA and reboot flows; exposes runtime_savecount in system status and updates web UI, locales, and types. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Main as Main (setup)
participant S as Scheduler
participant RD as RuntimeData
participant FS as LittleFS
participant API as Web API
participant UI as Webapp UI
Main->>RD: init(scheduler)
RD->>S: register periodic loop (1 min)
Main->>RD: read()
RD->>FS: read `/runtime.json`
FS-->>RD: data / error
RD-->>Main: read() result
Note over S,RD: Scheduled loop checks daily trigger window (00:05–10:00) and debounce/explicit triggers
User->>API: complete firmware upload
API->>RD: write(60)
RD->>FS: write `/runtime.json`
FS-->>RD: result
RD-->>API: write() result
User->>API: reboot command
API->>RD: write(60)
RD->>FS: write `/runtime.json`
UI->>API: GET /sysstatus
API->>RD: getWriteCountAndTimeString()
RD-->>API: "count / time" or " / no time"
API-->>UI: JSON includes `runtime_savecount`
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
include/RuntimeData.h (1)
33-40: Document atomic flags individuallyBoth
_readOKand_writeOKshare the same comment (“true if the last read/write operation was successful”), which makes it unclear which flag refers to which operation. Please update the comments (or rename the members) so each atomic’s responsibility is explicit.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
include/RuntimeData.h(1 hunks)src/RuntimeData.cpp(1 hunks)src/WebApi_firmware.cpp(2 hunks)src/WebApi_maintenance.cpp(2 hunks)src/WebApi_sysstatus.cpp(2 hunks)src/main.cpp(2 hunks)webapp/src/components/FirmwareInfo.vue(1 hunks)webapp/src/locales/de.json(1 hunks)webapp/src/locales/en.json(1 hunks)webapp/src/types/SystemStatus.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/WebApi_maintenance.cppsrc/WebApi_firmware.cppinclude/RuntimeData.hsrc/RuntimeData.cppsrc/WebApi_sysstatus.cppsrc/main.cpp
webapp/src/**/*.{js,jsx,ts,tsx,vue}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
webapp/src/**/*.{js,jsx,ts,tsx,vue}: Webapp source must pass ESLint (yarn lint)
Webapp source must be Prettier-formatted (yarn prettier --check src/)
Files:
webapp/src/components/FirmwareInfo.vuewebapp/src/types/SystemStatus.ts
🧬 Code graph analysis (1)
include/RuntimeData.h (1)
src/RuntimeData.cpp (16)
init(37-44)init(37-37)read(116-150)read(116-116)write(60-110)write(60-60)getWriteCount(156-160)getWriteCount(156-156)getWriteEpochTime(166-170)getWriteEpochTime(166-166)getWriteCountAndTimeString(178-191)getWriteCountAndTimeString(178-178)loop(50-54)loop(50-50)getWriteTrigger(197-211)getWriteTrigger(197-197)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build Environments (generic_esp32s3_usb)
- GitHub Check: Build Environments (generic_esp32s3)
- GitHub Check: Build Environments (generic_esp32_4mb_no_ota)
- GitHub Check: Build Environments (generic_esp32_8mb)
🔇 Additional comments (7)
webapp/src/types/SystemStatus.ts (1)
34-34: LGTM!The new
runtime_savecount: stringfield is correctly positioned and follows the existing naming conventions. The string type is appropriate for the combined "count / time" format exposed by the backend.src/WebApi_sysstatus.cpp (1)
17-17: LGTM!The addition of
RuntimeData.hinclude and the use ofRuntimeData.getWriteCountAndTimeString()to populate theruntime_savecountfield is straightforward and correct. This is a read-only operation with no side effects, appropriate for a status endpoint.Also applies to: 81-81
webapp/src/locales/en.json (1)
285-285: LGTM!The localization entry for
RuntimeSaveCountis clear, descriptive, and follows the existing pattern. The label "Runtime data save count / time" effectively communicates the combined information displayed.webapp/src/components/FirmwareInfo.vue (1)
86-89: LGTM with a minor observation.The new table row for
RuntimeSaveCountis correctly implemented and follows the existing pattern. The positioning betweenConfigSaveCountandUptimeis logical.One minor observation: Unlike
cfgsavecount(line 84) which uses$n(systemStatus.cfgsavecount, 'decimal')for number formatting,runtime_savecountis displayed directly. This appears intentional sinceruntime_savecountis a string containing both count and time information (as indicated by the localization "Runtime data save count / time"). No action needed unless the backend format changes.webapp/src/locales/de.json (1)
285-285: LGTM!The German localization entry for
RuntimeSaveCountis properly translated and follows the existing pattern. The label "Laufzeitdatenspeicherungen Anzahl / Zeit" accurately mirrors the English version.src/main.cpp (1)
157-159: Verify non-blocking read and clarify initialization order.
- Confirm that RuntimeData.read() completes quickly or handles missing/corrupt files without blocking startup.
- Add error handling or at least log the result of read() for visibility.
- Clarify the comment to explain why RuntimeData must be initialized last (e.g., it references other components or requires the scheduler to be fully configured).
src/WebApi_maintenance.cpp (1)
47-47: Handle write() return and assess blocking behavior
RuntimeData.write()returns a bool but isn’t checked here; wrap it in anifand log or act on failures.- LittleFS writes are synchronous, so verify that this call completes within acceptable time (<100 ms) or move it off the reboot path (e.g. perform the write before sending the response or offload it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/RuntimeData.cpp (1)
197-207: Guard_lastTriggerwith the class mutex
getWriteTrigger()flips_lastTriggerwithout taking_mutex, so concurrent callers race the write, violating the class’s thread-safety guarantee. Please take_mutexbefore touching_lastTriggerso all state transitions stay atomic.bool RuntimeClass::getWriteTrigger(void) { 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; - } + std::lock_guard<std::mutex> lock(_mutex); + 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; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
include/RuntimeData.h(1 hunks)src/RuntimeData.cpp(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- include/RuntimeData.h
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/RuntimeData.cpp
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build Environments (generic_esp32s3_usb)
- GitHub Check: Build Environments (generic_esp32_8mb)
- GitHub Check: Build Environments (generic_esp32s3)
- GitHub Check: Build Environments (generic_esp32_4mb_no_ota)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/RuntimeData.cpp (1)
206-219: Race condition:_lastTriggeraccessed without mutex protection.The
_lastTriggermember variable is read and modified (lines 210-211, 215) without mutex protection, creating a race condition. SincegetWriteTrigger()is called from the scheduled loop task and potentially from other contexts, concurrent access can corrupt the trigger state, leading to missed or duplicate writes.Apply this diff to add mutex protection:
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; }
🧹 Nitpick comments (1)
src/RuntimeData.cpp (1)
192-193: Consider clarifying the time availability check.Line 192 calls
getLocalTime(&time, 5)but immediately overwritestimewithlocaltime_r(&_writeEpoch, &time)on line 193. ThegetLocalTimecall is only used for its boolean return to verify the time service is available, which makes the code somewhat confusing.Consider extracting a helper method to check time service availability, or add a comment explaining that
getLocalTimeis used only as an availability check:+ // Check if time service is available before converting epoch to local time if ((_writeEpoch != 0) && (getLocalTime(&time, 5))) { localtime_r(&_writeEpoch, &time);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/RuntimeData.cpp(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/RuntimeData.cpp
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build Environments (generic_esp32s3_usb)
- GitHub Check: Build Environments (generic_esp32s3)
- GitHub Check: Build Environments (generic_esp32_4mb_no_ota)
- GitHub Check: Build Environments (generic_esp32_8mb)
🔇 Additional comments (1)
src/RuntimeData.cpp (1)
77-116: LGTM! Write state updates deferred until success.The implementation correctly addresses the previous review feedback. The write counter and epoch are now only committed after a successful file write (lines 111-116), preventing the rate limiter from being incorrectly updated when writes fail.
These contradict. The first is reasonable. The second should be something like "runtime data is also saved periodically, every hour". Is this still a shell? I don't see what data is actually saved. |
this depends on the actual data, doesn't it? i don't see why data like "battery last fully charged" or "calibration factors" should be written on an hourly basis. as they will change rarely. IMO, for such slow changing data, a single write a day and planned writes on reboot or update should be enough. and also there i think it would be smart to only write, when data was update/changed. i don't know if the filesystem handles this on its own to reduce unnecessary writecycles. |
|
Hello @schlimmchen and @spcqike
Yes, but I'm already testing the first function (saving the last SoC calibration time, 100% SoC).
I share @spcqike opinion. It depends on the information we want to save. And how critical it would be if we lose it. But, we also can do it more frequently or ... and that is the option I would prefer .. On demand. Would that be okay with you? 🤔 |
shouldn't the RuntimeClass::loop() handle this? Check, whether or not data was changed and save it accordingly? the loop() may run every minute or faster, but only write when a change is detected and when the last write was > 1h ago, or so. |
No, I think that is the wrong place.
This might be easy to decide with Boolean data. But when do we store changing values like the battery's internal resistance? However, so far we don't have any important data that needs to be saved to the runtime file so often. Normally during WebApp 'OTA firmware update' and 'Reset' is enough. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/RuntimeData.cpp (2)
197-198: Minor optimization: redundant getLocalTime call.Line 197 calls
getLocalTime(&time, 5)solely to check if the time service is available, but the resultingtimestruct is immediately overwritten bylocaltime_r(&_writeEpoch, &time)on line 198. While not incorrect, this is slightly wasteful.Consider checking time service availability without populating a struct that will be discarded:
- if ((_writeEpoch != 0) && (getLocalTime(&time, 5))) { + struct tm tempCheck; + if ((_writeEpoch != 0) && (getLocalTime(&tempCheck, 5))) { localtime_r(&_writeEpoch, &time);Or simply rely on
_writeEpoch != 0if time service availability is already implied by a non-zero epoch.
212-225: Consider protecting_lastTriggerwith mutex for design robustness.The comment on line 210 acknowledges that
_lastTriggeris accessed without mutex protection, relying on the assumption thatgetWriteTrigger()is only called from the single-threadedloop(). While this may be safe in the current implementation, it creates a fragile design:
- Member variables with underscore prefix conventionally require synchronization in multi-threaded contexts.
- If future code changes call
getWriteTrigger()from another thread (e.g., for debugging or manual trigger), a data race will occur.- The edge-detection logic (lines 216-221) reads and writes
_lastTriggerwithout atomic guarantees.Consider protecting
_lastTriggerwith_mutexto make the design more robust:bool RuntimeClass::getWriteTrigger(void) { struct tm actTime; if (getLocalTime(&actTime, 5)) { if ((actTime.tm_hour == 0) && (actTime.tm_min >= 5) && (actTime.tm_min <= 10)) { + std::lock_guard<std::mutex> lock(_mutex); if (_lastTrigger == false) { _lastTrigger = true; return true; } } else { + std::lock_guard<std::mutex> lock(_mutex); _lastTrigger = false; } } return false; }This adds minimal overhead (mutex is uncontended in single-threaded use) while preventing potential future data races.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
include/RuntimeData.h(1 hunks)src/RuntimeData.cpp(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- include/RuntimeData.h
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/RuntimeData.cpp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/RuntimeData.cpp (2)
63-122: Excellent write implementation with proper state management.The write method correctly addresses the previous review concern by staging the next state values and only committing them after successful file I/O. Rate limiting, mutex usage, and error handling are all properly implemented.
Consider making the rate-limit error message more specific:
- return cleanExit(false, "Time interval between 2 write operations too short, skipping write"); + return cleanExit(false, "Time interval between 2 write operations too short (requires %u minutes), skipping write", freezeMinutes);
190-206: Consider clarifying the time-checking logic.The intent of checking
getLocalTime()before converting_writeEpochis to ensure the time system is initialized. However, the code pattern of populatingtimeand then immediately overwriting it is confusing.Consider this clearer implementation:
String RuntimeClass::getWriteCountAndTimeString(void) const { std::lock_guard<std::mutex> lock(_mutex); char buf[32] = ""; struct tm time; + struct tm timeCheck; - if ((_writeEpoch != 0) && (getLocalTime(&time, 5))) { + if ((_writeEpoch != 0) && (getLocalTime(&timeCheck, 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; }This makes it explicit that
getLocalTime()is only used to verify time system availability, whiletimeis independently populated from_writeEpoch.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/RuntimeData.cpp(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/RuntimeData.cpp
🧬 Code graph analysis (1)
src/RuntimeData.cpp (2)
src/WebApi_maintenance.cpp (2)
init(13-18)init(13-13)src/WebApi_firmware.cpp (2)
init(15-29)init(15-15)
🔇 Additional comments (5)
src/RuntimeData.cpp (5)
1-33: LGTM! Clear documentation and proper setup.The file header provides comprehensive documentation of the RuntimeData functionality. The TAG constant is appropriately set to "runtimedata", constants are well-defined, and the singleton pattern is correctly implemented.
39-46: LGTM! Proper scheduler task initialization.The init method correctly sets up the periodic loop task with appropriate interval and callback binding.
52-56: LGTM! Clean loop implementation.The loop method correctly checks the daily trigger condition and invokes write with a 60-minute debounce interval.
128-162: LGTM! Robust read implementation with proper error handling.The read method gracefully handles missing files and deserialization errors, applies appropriate defaults, and correctly uses mutex protection for state updates.
212-226: LGTM! Trigger logic correctly uses instance state.The getWriteTrigger method properly uses
_lastTriggeras a class member (addressing the previous review concern about static state) and correctly implements debounce logic for the daily trigger window with appropriate mutex protection.
|
Hallo @AndreasBoehm ,
|
|
@SW-Niko Das geht leider nicht, da deine RuntimeData branch nicht im hoylabs repo sondern in deinem Fork liegt. Du kannst |
read() must be done after all other components have been initialized
|
@AndreasBoehm Danke für die Info. Das erklärt auch warum der Battery Gard in der Liste angeboten wird. 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
include/RuntimeData.h(1 hunks)src/RuntimeData.cpp(1 hunks)src/WebApi_firmware.cpp(2 hunks)src/WebApi_maintenance.cpp(2 hunks)src/WebApi_sysstatus.cpp(2 hunks)src/main.cpp(2 hunks)webapp/src/components/FirmwareInfo.vue(1 hunks)webapp/src/locales/de.json(1 hunks)webapp/src/locales/en.json(1 hunks)webapp/src/types/SystemStatus.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- webapp/src/components/FirmwareInfo.vue
- src/WebApi_sysstatus.cpp
- webapp/src/locales/en.json
- webapp/src/types/SystemStatus.ts
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/WebApi_maintenance.cppsrc/WebApi_firmware.cppinclude/RuntimeData.hsrc/RuntimeData.cppsrc/main.cpp
🧬 Code graph analysis (2)
include/RuntimeData.h (3)
src/RuntimeData.cpp (16)
init(39-46)init(39-39)read(128-162)read(128-128)write(63-122)write(63-63)getWriteCount(168-172)getWriteCount(168-168)getWriteEpochTime(178-182)getWriteEpochTime(178-178)getWriteCountAndTimeString(190-206)getWriteCountAndTimeString(190-190)loop(52-56)loop(52-52)getWriteTrigger(212-226)getWriteTrigger(212-212)src/WebApi_firmware.cpp (2)
init(15-29)init(15-15)src/WebApi_maintenance.cpp (2)
init(13-18)init(13-13)
src/RuntimeData.cpp (2)
src/WebApi_firmware.cpp (2)
init(15-29)init(15-15)src/WebApi_maintenance.cpp (2)
init(13-18)init(13-13)
🔇 Additional comments (17)
webapp/src/locales/de.json (1)
285-285: LGTM!The German translation for
RuntimeSaveCountis accurate and follows the existing localization pattern in the file.src/WebApi_firmware.cpp (2)
13-13: LGTM!The include is properly added for RuntimeData usage.
51-53: LGTM!The RuntimeData.write(60) call is correctly placed after the HTTP response is sent but before the restart is triggered. This ensures runtime data is persisted during OTA updates without blocking the upload handler. The return value can be safely ignored here since the device will restart immediately.
src/main.cpp (1)
39-39: LGTM!The include is properly added for RuntimeData usage.
src/WebApi_maintenance.cpp (2)
11-11: LGTM!The include is properly added for RuntimeData usage.
47-49: LGTM!The RuntimeData.write(60) call follows the same pattern as in
src/WebApi_firmware.cpp, correctly placed after the HTTP response is sent but before the restart is triggered. This ensures runtime data is persisted during device reboots without blocking the request handler.src/RuntimeData.cpp (7)
1-31: LGTM!The file header provides clear documentation of the module's purpose, behavior, and constraints. The constants and TAG are well-defined.
39-46: LGTM!The
init()method correctly sets up the scheduler task with appropriate configuration:
- Runs every minute
- Properly binds the loop callback
- Infinite iterations (TASK_FOREVER)
52-56: LGTM!The
loop()method implements the daily trigger mechanism cleanly by checking for the trigger condition and invoking write with the appropriate throttle parameter.
63-122: LGTM!The
write()method is well-implemented with proper error handling and thread safety:
- Correctly validates local time availability before writing
- Implements throttling to prevent excessive writes
- Uses staging variables (nextCount, nextEpoch) and only commits them to class state after successful write
- Proper mutex protection for shared state
- Clear error messages via cleanExit lambda
- The past review concern about deferring updates until write succeeds has been properly addressed (lines 114-119)
128-162: LGTM!The
read()method is well-implemented with proper defaults and thread safety:
- Gracefully handles file not found or deserialization errors by using default values
- Proper mutex protection when updating shared state
- Uses
Utils::skipBom()for BOM handling- Returns success status while always ensuring valid defaults
- Clear logging of outcomes
168-206: LGTM!The accessor methods (
getWriteCount(),getWriteEpochTime(),getWriteCountAndTimeString()) are properly implemented with:
- Consistent mutex protection for thread-safe access
getWriteCountAndTimeString()correctly handles cases where time is not yet available- Appropriate time formatting for UI display
212-226: LGTM!The
getWriteTrigger()method correctly implements the daily trigger window (00:05-00:10) with proper edge detection using_lastTriggerto prevent multiple triggers on the same day. The past review concern about moving this from a static local to a class member has been properly addressed.include/RuntimeData.h (4)
1-8: LGTM!The includes are appropriate for the class design:
- ArduinoJson for serialization
- TaskSchedulerDeclarations for task management
- mutex and atomic for thread safety
10-27: LGTM!The public interface is well-designed:
- Explicit constructor prevents implicit conversions
- Copy and move operations properly deleted for singleton pattern
init()for scheduler setupread()andwrite()for persistence with throttling support- Clean accessor methods for querying state
getWriteState()andgetReadState()provide operation status feedbackgetWriteCountAndTimeString()provides formatted data for UI display
29-41: LGTM!The private implementation details are well-structured:
- Task for periodic execution
- Atomics for operation status (thread-safe without mutex overhead for simple flags)
- Mutable mutex for protecting shared data in const methods
- State tracking for trigger detection and versioning
- Clear separation between public API and internal implementation
43-43: LGTM!The extern declaration properly exposes the global singleton instance that is defined in
src/RuntimeData.cpp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
src/RuntimeData.cpp (1)
69-129: LGTM! Write method correctly implements transactional semantics.The implementation properly addresses the past critical issue: values are staged locally (line 95), I/O is performed, and state is committed only after a successful write (lines 122-124). The throttle logic correctly allows bypassing when
freezeMinutes=0. The cleanExit lambda provides consistent logging and state management.Minor clarity suggestion: The comment on line 103 says "protect the shared data with a local mutex" but the mutex is already held at this point. Consider rewording to "ensure any additional shared data added here remains under the existing mutex protection."
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
include/RuntimeData.h(1 hunks)src/RuntimeData.cpp(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
C/C++ code must pass cpplint with the specified filters
Files:
src/RuntimeData.cppinclude/RuntimeData.h
🧠 Learnings (1)
📚 Learning: 2025-10-17T08:29:40.184Z
Learnt from: SW-Niko
PR: hoylabs/OpenDTU-OnBattery#2262
File: src/main.cpp:149-159
Timestamp: 2025-10-17T08:29:40.184Z
Learning: In the OpenDTU-OnBattery project (src/main.cpp), RuntimeData must be initialized after SolarCharger, PowerMeter, PowerLimiter, GridCharger, and Battery components have been initialized, but before the main loop. RuntimeData acts as a persistence service that these components access during their runtime operations (not during initialization).
Applied to files:
src/RuntimeData.cpp
🧬 Code graph analysis (2)
src/RuntimeData.cpp (2)
src/WebApi_firmware.cpp (2)
init(15-29)init(15-15)src/WebApi_maintenance.cpp (2)
init(13-18)init(13-13)
include/RuntimeData.h (1)
src/RuntimeData.cpp (16)
init(41-48)init(41-41)read(135-169)read(135-135)write(69-129)write(69-69)getWriteCount(175-179)getWriteCount(175-175)getWriteEpochTime(185-189)getWriteEpochTime(185-185)getWriteCountAndTimeString(197-213)getWriteCountAndTimeString(197-197)loop(54-62)loop(54-54)getWriteTrigger(219-236)getWriteTrigger(219-219)
🪛 Clang (14.0.6)
src/RuntimeData.cpp
[error] 20-20: 'Utils.h' file not found
(clang-diagnostic-error)
include/RuntimeData.h
[error] 4-4: 'ArduinoJson.h' file not found
(clang-diagnostic-error)
🔇 Additional comments (8)
include/RuntimeData.h (2)
10-43: LGTM! Thread-safe singleton design is solid.The class correctly implements a non-copyable/non-movable singleton pattern. The synchronization strategy properly separates atomic flags (
_readOK,_writeOK,_writeNow) from mutex-protected shared state (_writeVersion,_writeCount,_writeEpoch,_lastTrigger). The mutable mutex appropriately enables const methods to acquire locks.
19-28: LGTM! Public interface is clean and well-designed.The API provides clear methods for initialization, persistence operations, and state queries. The default 10-minute throttle in
write()and the inlinerequestWriteOnNextTaskLoop()setter are sensible design choices. Const correctness is properly maintained throughout.src/RuntimeData.cpp (6)
1-35: LGTM! Clean module setup with clear documentation.The file header provides excellent guidance for future maintainers, including deadlock avoidance advice. The TAG is now correctly set to "runtimedata" (addressing past feedback), and the constants and singleton initialization are properly defined.
41-62: LGTM! Task initialization and loop logic are correct.The minute-based task is properly configured, and the loop logic correctly triggers writes either on explicit request (
_writeNow) or during the daily maintenance window. Callingwrite(0)bypasses the throttle for these scheduled events, which is the intended behavior.
135-169: LGTM! Read method properly handles missing or corrupt data.The implementation gracefully falls back to defaults when the file is missing or JSON deserialization fails. Mutex protection correctly guards shared state updates (lines 152-156), and the default operator
|is the idiomatic way to provide fallback values with ArduinoJson.(Same optional comment clarity note as in the write method applies to line 158.)
175-189: LGTM! Accessors properly protect shared state.Both getters correctly acquire the mutex before returning the protected values.
197-213: LGTM! Time formatting handles unavailable time server gracefully.The method correctly verifies time server availability (line 205) before converting the epoch to local time (line 206). The fallback to "no time" is appropriate for early boot scenarios.
219-236: LGTM! Daily trigger window is correctly implemented.The method properly detects the 00:05–00:10 window and uses
_lastTriggerfor edge detection to prevent multiple fires. This addresses the past review concern about the static flag—_lastTriggeris now a proper instance member protected by the mutex.
|
Hello @schlimmchen or @AndreasBoehm ,
But I need this PR and #2298 first to avoid triple nested PRs. 😀 |

Read and write runtime data persistent on LittleFS
I should also mention the following points:
See issue #1676
To quickly test the runtime functionality on your system, follow these steps: