Skip to content

Conversation

@SW-Niko
Copy link

@SW-Niko SW-Niko commented Oct 1, 2025

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.
  • Thread save access to the data is provided by a mutex/lock.
  • Indicate the save count and the last save time in the web UI
grafik

I should also mention the following points:

  • The mutex only protects the RuntimeData class's own data
  • I simply attached it to the OTA update callback and reset callback function to save the runtime data

See issue #1676

To quickly test the runtime functionality on your system, follow these steps:

  • Update your system with the new firmware.
  • Go to "Info" -> "System" and check "Runtime data save count / time 0 / no time"
  • Force the system to save the runtime data on a JSON-File by doing a reboot via the web interface.
  • Again go to "Info" -> "System" and check "Runtime data save count / time 1 / 15-Oct 19:33"
  • You can also download the JSON-File and review its content.

{
    "info": {
        "version": 0,
        "save_count": 1,
        "save_epoch": 1760549624
    }
}

@github-actions
Copy link

github-actions bot commented Oct 1, 2025

Build Artifacts

Firmware built from this pull request's code:

Notice

  • These artifacts are ZIP files containing the factory update binary as well as the OTA update binary.
    Extract the binaries from the ZIP files first. Do not use the ZIP files themselves to perform an update.
  • These links point to artifacts of the latest successful build run.
  • The linked artifacts were built from 947e5e1.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 8, 2025

Runtime data also written to LittleFS if WebUI 'Device Reboot' is used.

@coderabbitai
Copy link

coderabbitai bot commented Oct 11, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Runtime data module
include/RuntimeData.h, src/RuntimeData.cpp
New RuntimeClass and global RuntimeData: ctor, init(Scheduler&), read(), write(uint16_t), scheduled minute loop() with daily-window + debounce/trigger, mutex/atomic state, JSON persistence to /runtime.json via LittleFS, and accessors (getWriteCount, getWriteEpochTime, getWriteCountAndTimeString, getReadState, getWriteState, requestWriteOnNextTaskLoop).
Web API integrations
src/WebApi_firmware.cpp, src/WebApi_maintenance.cpp, src/WebApi_sysstatus.cpp
Include RuntimeData.h; call RuntimeData.write(60) after OTA finish and after successful reboot response (throttled); add runtime_savecount to sysstatus JSON using getWriteCountAndTimeString().
App initialization
src/main.cpp
Include RuntimeData.h; invoke RuntimeData.init(scheduler) and RuntimeData.read() during setup after component initializations.
Webapp UI
webapp/src/components/FirmwareInfo.vue
Add a table row to display systemStatus.runtime_savecount.
Locales
webapp/src/locales/en.json, webapp/src/locales/de.json
Add firmwareinfo.RuntimeSaveCount translation entries (English and German).
Type definitions
webapp/src/types/SystemStatus.ts
Add runtime_savecount: string to the SystemStatus interface.

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`
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through bytes and tuck them tight,
LittleFS burrow saved by night.
Scheduler ticks, I mark each store,
Count and time — I keep the score. 🐇

Pre-merge checks and finishing touches

✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed The pull request description accurately describes the implementation of a persistent runtime data storage system on LittleFS. The description claims that data is stored in JSON format with writes triggered during OTA firmware upgrade and reboot operations, plus daily writes at 00:05 with a 1-hour throttling mechanism and mutex-protected thread-safe access. These claims align with the changeset: the header file defines the RuntimeClass with thread-safe members and accessor methods; the implementation file includes the write logic with time-window checks and mutex protection; integration files add write calls in firmware update and reboot endpoints; and UI components display the save count and timestamp. The description provides specific technical details, testing instructions, and example JSON output, demonstrating a clear understanding of the implemented functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 individually

Both _readOK and _writeOK share 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

📥 Commits

Reviewing files that changed from the base of the PR and between af39f5a and a33bcd6.

📒 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.cpp
  • src/WebApi_firmware.cpp
  • include/RuntimeData.h
  • src/RuntimeData.cpp
  • src/WebApi_sysstatus.cpp
  • src/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.vue
  • webapp/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: string field 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.h include and the use of RuntimeData.getWriteCountAndTimeString() to populate the runtime_savecount field 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 RuntimeSaveCount is 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 RuntimeSaveCount is correctly implemented and follows the existing pattern. The positioning between ConfigSaveCount and Uptime is logical.

One minor observation: Unlike cfgsavecount (line 84) which uses $n(systemStatus.cfgsavecount, 'decimal') for number formatting, runtime_savecount is displayed directly. This appears intentional since runtime_savecount is 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 RuntimeSaveCount is 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 an if and 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).

Copy link

@coderabbitai coderabbitai bot left a 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 _lastTrigger with the class mutex

getWriteTrigger() flips _lastTrigger without taking _mutex, so concurrent callers race the write, violating the class’s thread-safety guarantee. Please take _mutex before touching _lastTrigger so 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

📥 Commits

Reviewing files that changed from the base of the PR and between a33bcd6 and f9449cb.

📒 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)

Copy link

@coderabbitai coderabbitai bot left a 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: _lastTrigger accessed without mutex protection.

The _lastTrigger member variable is read and modified (lines 210-211, 215) without mutex protection, creating a race condition. Since getWriteTrigger() 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 overwrites time with localtime_r(&_writeEpoch, &time) on line 193. The getLocalTime call 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 getLocalTime is 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

📥 Commits

Reviewing files that changed from the base of the PR and between f9449cb and 960662f.

📒 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.

@schlimmchen
Copy link
Member

The data is written once a day at 00:05, during WebApp 'OTA firmware update' and 'Reset'
The data is not written if last write was less then 1 hour ago.

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.

@spcqike
Copy link

spcqike commented Oct 13, 2025

runtime data is also saved periodically, every hour

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.

see #1676 and #1466

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.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 13, 2025

Hello @schlimmchen and @spcqike
let me answer your questions. 😀

Is this still a shell?

Yes, but I'm already testing the first function (saving the last SoC calibration time, 100% SoC).

The second should be something like "runtime data is also saved periodically, every hour".

I share @spcqike opinion. It depends on the information we want to save. And how critical it would be if we lose it.
Actual I just have slow changing data in mind and a single safety write a day should be enough.

But, we also can do it more frequently or ... and that is the option I would prefer .. On demand.
For example: if we want to save the "battery charge circle" state we can call RuntimeData.write() on every change.
(Requires a small code modification to disable the 1 hour freeze)

Would that be okay with you? 🤔

@spcqike
Copy link

spcqike commented Oct 13, 2025

and that is the option I would prefer .. On demand.
For example: if we want to save the "battery charge circle" state we can call RuntimeData.write() on every change.

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.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 13, 2025

shouldn't the RuntimeClass::loop() handle this?

No, I think that is the wrong place.

Check, whether or not data was changed and save it accordingly?

This might be easy to decide with Boolean data. But when do we store changing values ​​like the battery's internal resistance?
12.05 mOhm. Every time the last digit changes? I would leave that decision to the data owner function.

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.
An just for cases like 'supply power drop' or 'serial firmware update' or 'reset by firmware bug' or I added 1 safety write a day.

Copy link

@coderabbitai coderabbitai bot left a 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 resulting time struct is immediately overwritten by localtime_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 != 0 if time service availability is already implied by a non-zero epoch.


212-225: Consider protecting _lastTrigger with mutex for design robustness.

The comment on line 210 acknowledges that _lastTrigger is accessed without mutex protection, relying on the assumption that getWriteTrigger() is only called from the single-threaded loop(). While this may be safe in the current implementation, it creates a fragile design:

  1. Member variables with underscore prefix conventionally require synchronization in multi-threaded contexts.
  2. If future code changes call getWriteTrigger() from another thread (e.g., for debugging or manual trigger), a data race will occur.
  3. The edge-detection logic (lines 216-221) reads and writes _lastTrigger without atomic guarantees.

Consider protecting _lastTrigger with _mutex to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2c15277 and 5a7eaec.

📒 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

Copy link

@coderabbitai coderabbitai bot left a 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 _writeEpoch is to ensure the time system is initialized. However, the code pattern of populating time and 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, while time is independently populated from _writeEpoch.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7eaec and 8772c31.

📒 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 _lastTrigger as 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.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 15, 2025

Hallo @AndreasBoehm ,
vielleicht kannst du mir einen Tipp geben wie ich folgendes Problem lösen kann.
Ich habe das nächste Feature fertig und das setzt den Runtime Branch auf.
Aber ich kann als Basis Branch 'Runtime' nicht auswählen und wenn ich "development' nehme dann wird 'Runtime' mit eingebunden.
Mach ich was falsch? Oder muss 'Runtime erst in 'development' ge-merged werden? 🤔

grafik

@AndreasBoehm
Copy link
Member

@SW-Niko Das geht leider nicht, da deine RuntimeData branch nicht im hoylabs repo sondern in deinem Fork liegt.

Du kannst development als base für deinen PR nehmen und erwähnen das er auf diesem hier basiert. Anders können wir das mit einem Fork leider nicht lösen.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 16, 2025

@AndreasBoehm Danke für die Info. Das erklärt auch warum der Battery Gard in der Liste angeboten wird. 👍

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8772c31 and 8982927.

📒 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.cpp
  • src/WebApi_firmware.cpp
  • include/RuntimeData.h
  • src/RuntimeData.cpp
  • src/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 RuntimeSaveCount is 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 _lastTrigger to 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 setup
  • read() and write() for persistence with throttling support
  • Clean accessor methods for querying state
  • getWriteState() and getReadState() provide operation status feedback
  • getWriteCountAndTimeString() 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.

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8982927 and 97ffc77.

📒 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.cpp
  • include/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 inline requestWriteOnNextTaskLoop() 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. Calling write(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 _lastTrigger for edge detection to prevent multiple fires. This addresses the past review concern about the static flag—_lastTrigger is now a proper instance member protected by the mutex.

@SW-Niko
Copy link
Author

SW-Niko commented Oct 20, 2025

Hello @schlimmchen or @AndreasBoehm ,
Do you see any chance of reviewing this PR in the near future?
I've expanded the Battery Guard with:

  • Write the calculated DC-Pulse value into the runtime data
  • Stop-Voltage Power Limiter
  • Recharge Helper

But I need this PR and #2298 first to avoid triple nested PRs. 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants