#include "PHApp.h" #include "config.h" #include #include #include #ifdef ENABLE_PROFILE_TEMPERATURE #include #include #endif short PHApp::load(short val0, short val1) { Log.infoln(F("PHApp::load() - Loading application data...")); Log.infoln(F("PHApp::load() - Attempting to load temperature profiles...")); if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted (true formats if necessary) Log.errorln(F("PHApp::load() - Failed to mount LittleFS. Cannot load profiles.")); return E_INVALID_PARAMETER; // Use invalid parameter as fallback } const char *filename = "/profiles/defaults.json"; // Path in LittleFS File file = LittleFS.open(filename, "r"); if (!file) { Log.errorln(F("PHApp::load() - Failed to open profile file: %s"), filename); LittleFS.end(); // Close LittleFS return E_NOT_FOUND; // Use standard not found } // Increased size slightly for safety, adjust if needed // DynamicJsonDocument doc(JSON_ARRAY_SIZE(PROFILE_TEMPERATURE_COUNT) + PROFILE_TEMPERATURE_COUNT * JSON_OBJECT_SIZE(5 + MAX_TEMP_CONTROL_POINTS)); // Replace DynamicJsonDocument with JsonDocument, letting it handle allocation. JsonDocument doc; // Deserialize the JSON document DeserializationError error = deserializeJson(doc, file); file.close(); // Close the file ASAP LittleFS.end(); // Close LittleFS if (error) { Log.errorln(F("PHApp::load() - Failed to parse profile JSON: %s"), error.c_str()); return E_INVALID_PARAMETER; // Use invalid parameter } // Check if the root is a JSON array if (!doc.is()) { Log.errorln(F("PHApp::load() - Profile JSON root is not an array.")); return E_INVALID_PARAMETER; // Use invalid parameter } JsonArray profilesArray = doc.as(); Log.infoln(F("PHApp::load() - Found %d profiles in JSON file."), profilesArray.size()); uint8_t profileIndex = 0; for (JsonObject profileJson : profilesArray) { if (profileIndex >= PROFILE_TEMPERATURE_COUNT) { Log.warningln(F("PHApp::load() - Too many profiles in JSON (%d), only loading the first %d."), profilesArray.size(), PROFILE_TEMPERATURE_COUNT); break; } if (!tempProfiles[profileIndex]) { Log.errorln(F("PHApp::load() - TemperatureProfile slot %d is not initialized. Skipping JSON profile."), profileIndex); // Don't increment profileIndex here, try to load next JSON into same slot if possible? // Or increment profileIndex to align JSON index with slot index? Let's align. profileIndex++; continue; } // Assuming TemperatureProfile (or its base PlotBase) has a public method // like loadFromJson that takes the JsonObject and calls the protected virtual load. // We also assume it returns bool or short (E_OK for success). Log.infoln(F("PHApp::load() - Loading JSON data into TemperatureProfile slot %d..."), profileIndex); // Now call the protected load() directly, as PHApp is a friend if (tempProfiles[profileIndex]->load(profileJson)) { // returns bool const char *name = profileJson["name"] | "Unnamed"; // Get name for logging Log.infoln(F("PHApp::load() - Successfully loaded profile '%s' into slot %d."), name, profileIndex); } else { Log.errorln(F("PHApp::load() - Failed to load profile data into slot %d."), profileIndex); // Decide if we should return an error or just continue loading others // return E_INVALID_PARAMETER; // Option: Stop loading on first failure } Log.infoln(F("PHApp::load() - Loaded %d profiles from JSON into %d available slots and %d target registers."), profileIndex, PROFILE_TEMPERATURE_COUNT, tempProfiles[profileIndex]->getTargetRegisters().size()); profileIndex++; // Move to the next TemperatureProfile slot } // Handle case where JSON has fewer profiles than allocated slots if (profileIndex < profilesArray.size()) { Log.warningln(F("PHApp::load() - Processed %d JSON profiles but only %d slots were available/initialized."), profilesArray.size(), profileIndex); } else if (profileIndex < PROFILE_TEMPERATURE_COUNT) { Log.infoln(F("PHApp::load() - Loaded %d profiles from JSON into %d available slots."), profileIndex, PROFILE_TEMPERATURE_COUNT); } // --- Load Signal Plot Profiles --- #ifdef ENABLE_PROFILE_SIGNAL_PLOT Log.infoln(F("PHApp::load() - Attempting to load signal plot profiles...")); if (!LittleFS.begin(true)) { // Ensure LittleFS is mounted, or re-mount if it was closed Log.errorln(F("PHApp::load() - Failed to mount LittleFS for signal plots. Cannot load signal profiles.")); // Decide on return strategy: return E_INVALID_PARAMETER or continue? // For now, let's log and continue, as temp profiles might have loaded. } else { const char *signalPlotFilename = "/profiles/signal_plots.json"; File signalPlotFile = LittleFS.open(signalPlotFilename, "r"); if (!signalPlotFile) { Log.errorln(F("PHApp::load() - Failed to open signal plot profile file: %s. This might be normal if it doesn't exist yet."), signalPlotFilename); } else { JsonDocument signalPlotDoc; // Use a new document for signal plots DeserializationError spError = deserializeJson(signalPlotDoc, signalPlotFile); signalPlotFile.close(); if (spError) { Log.errorln(F("PHApp::load() - Failed to parse signal plot JSON: %s"), spError.c_str()); } else if (!signalPlotDoc.is()) { Log.errorln(F("PHApp::load() - Signal plot JSON root is not an array.")); } else { JsonArray spArray = signalPlotDoc.as(); Log.infoln(F("PHApp::load() - Found %d signal plot profiles in JSON file."), spArray.size()); uint8_t spIndex = 0; for (JsonObject spJson : spArray) { if (spIndex >= PROFILE_SIGNAL_PLOT_COUNT) { Log.warningln(F("PHApp::load() - Too many signal plot profiles in JSON (%d), only loading the first %d."), spArray.size(), PROFILE_SIGNAL_PLOT_COUNT); break; } if (signalPlots[spIndex]) { Log.infoln(F("PHApp::load() - Loading JSON data into SignalPlot slot %d..."), spIndex); if (signalPlots[spIndex]->load(spJson)) { const char *spName = spJson["name"] | "Unnamed Signal Plot"; Log.infoln(F("PHApp::load() - Successfully loaded signal plot profile '%s' into slot %d."), spName, spIndex); } else { Log.errorln(F("PHApp::load() - Failed to load signal plot profile data into slot %d."), spIndex); } } else { Log.errorln(F("PHApp::load() - SignalPlot slot %d is not initialized. Skipping JSON profile."), spIndex); } spIndex++; } Log.infoln(F("PHApp::load() - Loaded %d signal plot profiles from JSON into %d available slots."), spIndex, PROFILE_SIGNAL_PLOT_COUNT); } } LittleFS.end(); // Close LittleFS after signal plots are done } #endif // ENABLE_PROFILE_SIGNAL_PLOT return E_OK; } #ifdef ENABLE_PROFILE_TEMPERATURE /** * @brief Handles GET requests to /api/v1/profiles * Returns a list of available temperature profile slots. */ void PHApp::getProfilesHandler(AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); JsonDocument doc; JsonArray profilesArray = doc["profiles"].to(); for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) { TemperatureProfile *profile = this->tempProfiles[i]; if (profile) { Log.verboseln(" Processing Profile Slot %d: %s", i, profile->name.c_str()); JsonObject profileObj = profilesArray.add(); profileObj["slot"] = i; profileObj["duration"] = profile->getDuration(); profileObj["status"] = (int)profile->getCurrentStatus(); profileObj["currentTemp"] = profile->getTemperature(profile->getElapsedMs()); profileObj["name"] = profile->name; profileObj["max"] = profile->max; profileObj["enabled"] = profile->enabled(); profileObj["elapsed"] = profile->getElapsedMs(); profileObj["remaining"] = profile->getRemainingTime(); profileObj["signalPlot"] = profile->getSignalPlotSlotId(); JsonArray pointsArray = profileObj["controlPoints"].to(); const TempControlPoint *points = profile->getTempControlPoints(); uint8_t numPoints = profile->getNumTempControlPoints(); for (uint8_t j = 0; j < numPoints; ++j) { JsonObject pointObj = pointsArray.add(); pointObj["x"] = points[j].x; pointObj["y"] = points[j].y; } JsonArray targetRegistersArray = profileObj["targetRegisters"].to(); const std::vector &targets = profile->getTargetRegisters(); for (uint16_t targetReg : targets) { targetRegistersArray.add(targetReg); } } else { Log.warningln(" Profile slot %d is null", i); } } serializeJson(doc, *response); request->send(response); } /** * @brief Handles POST requests to /api/v1/profiles/{slot} * Updates the specified temperature profile using the provided JSON data. * * @param request The incoming web request. * @param json The parsed JSON body from the request. * @param slot The profile slot number extracted from the URL. */ void PHApp::setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot) { if (slot < 0 || slot >= PROFILE_TEMPERATURE_COUNT) { Log.warningln("REST: setProfileHandler - Invalid slot number %d provided.", slot); request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid profile slot number\"}"); return; } // Check if the profile object exists for this slot TemperatureProfile *targetProfile = this->tempProfiles[slot]; if (!targetProfile) { Log.warningln("REST: setProfileHandler - No profile found for slot %d.", slot); request->send(404, "application/json", "{\"success\":false,\"error\":\"Profile slot not found or not initialized\"}"); return; } // Check if the JSON is an object if (!json.is()) { Log.warningln("REST: setProfileHandler - Invalid JSON payload (not an object) for slot %d.", slot); request->send(400, "application/json", "{\"success\":false,\"error\":\"Invalid JSON payload: must be an object.\"}"); return; } JsonObject jsonObj = json.as(); // Attempt to load the configuration into the profile object bool success = targetProfile->load(jsonObj); if (success) { Log.infoln("REST: Profile slot %d updated successfully.", slot); // Attempt to save all profiles back to JSON if (saveProfilesToJson()) { Log.infoln("REST: All profiles saved to JSON successfully after update."); request->send(200, "application/json", "{\"success\":true, \"message\":\"Profile updated and saved.\"}"); } else { Log.errorln("REST: Profile slot %d updated, but failed to save all profiles to JSON.", slot); request->send(500, "application/json", "{\"success\":true, \"message\":\"Profile updated but failed to save configuration.\"}"); // Send 200 as profile was updated, but indicate save error } } else { Log.errorln("REST: Failed to update profile slot %d from JSON.", slot); // Provide a more specific error if `load` can indicate the reason request->send(400, "application/json", "{\"success\":false,\"error\":\"Failed to load profile data. Check format and values.\"}"); } } bool PHApp::saveProfilesToJson() { Log.infoln(F("PHApp::saveProfilesToJson() - Saving all temperature profiles to JSON...")); if (!LittleFS.begin(true)) { Log.errorln(F("PHApp::saveProfilesToJson() - Failed to mount LittleFS. Cannot save profiles.")); return false; } const char *filename = "/profiles/defaults.json"; // Path in LittleFS JsonDocument doc; // Use a single JsonDocument for the array JsonArray profilesArray = doc.to(); for (int i = 0; i < PROFILE_TEMPERATURE_COUNT; ++i) { TemperatureProfile *profile = this->tempProfiles[i]; if (profile) { JsonObject profileJson = profilesArray.add(); profileJson["type"] = "temperature"; // Assuming this is fixed for now profileJson["slot"] = i; profileJson["name"] = profile->name; profileJson["duration"] = profile->getDuration(); // Duration in ms profileJson["max"] = profile->max; profileJson["signalPlot"] = profile->getSignalPlotSlotId(); // controlPoints JsonArray pointsArray = profileJson.createNestedArray("controlPoints"); const TempControlPoint *points = profile->getTempControlPoints(); uint8_t numPoints = profile->getNumTempControlPoints(); for (uint8_t j = 0; j < numPoints; ++j) { JsonObject pointObj = pointsArray.add(); pointObj["x"] = points[j].x; pointObj["y"] = points[j].y; } // targetRegisters JsonArray targetRegistersArray = profileJson.createNestedArray("targetRegisters"); const std::vector &targets = profile->getTargetRegisters(); for (uint16_t targetReg : targets) { targetRegistersArray.add(targetReg); } } else { Log.warningln(F("PHApp::saveProfilesToJson() - Profile slot %d is null, skipping."), i); } } File file = LittleFS.open(filename, "w"); // Open for writing, creates if not exists, truncates if exists if (!file) { Log.errorln(F("PHApp::saveProfilesToJson() - Failed to open profile file for writing: %s"), filename); LittleFS.end(); return false; } size_t bytesWritten = serializeJson(doc, file); file.close(); LittleFS.end(); if (bytesWritten > 0) { Log.infoln(F("PHApp::saveProfilesToJson() - Successfully wrote %d bytes to %s"), bytesWritten, filename); return true; } else { Log.errorln(F("PHApp::saveProfilesToJson() - Failed to serialize JSON or write to file: %s"), filename); return false; } } #endif // ENABLE_PROFILE_TEMPERATURE #ifdef ENABLE_PROFILE_SIGNAL_PLOT bool PHApp::saveSignalPlotsToJson() { Log.infoln(F("PHApp::saveSignalPlotsToJson() - Saving all SignalPlot profiles to JSON...")); if (!LittleFS.begin(true)) { Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to mount LittleFS. Cannot save profiles.")); return false; } const char *filename = "/profiles/signal_plots.json"; // Corrected filename JsonDocument doc; JsonArray profilesArray = doc.to(); for (int i = 0; i < PROFILE_SIGNAL_PLOT_COUNT; ++i) { SignalPlot *profile = this->signalPlots[i]; if (profile) { JsonObject profileJson = profilesArray.add(); profileJson["type"] = "signal"; profileJson["slot"] = i; profileJson["name"] = profile->name; profileJson["duration"] = profile->getDuration(); JsonArray pointsArray = profileJson.createNestedArray("controlPoints"); const S_SignalControlPoint *points = profile->getControlPoints(); uint8_t numPoints = profile->getNumControlPoints(); for (uint8_t j = 0; j < numPoints; ++j) { JsonObject pointObj = pointsArray.add(); pointObj["id"] = points[j].id; pointObj["time"] = points[j].time; pointObj["name"] = points[j].name; pointObj["description"] = points[j].description; pointObj["state"] = (int16_t)points[j].state; pointObj["type"] = (int16_t)points[j].type; pointObj["arg_0"] = points[j].arg_0; pointObj["arg_1"] = points[j].arg_1; pointObj["arg_2"] = points[j].arg_2; } } else { Log.warningln(F("PHApp::saveSignalPlotsToJson() - SignalPlot slot %d is null, skipping."), i); } } File file = LittleFS.open(filename, "w"); if (!file) { Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to open profile file for writing: %s"), filename); LittleFS.end(); return false; } size_t bytesWritten = serializeJson(doc, file); file.close(); LittleFS.end(); if (bytesWritten > 0) { Log.infoln(F("PHApp::saveSignalPlotsToJson() - Successfully wrote %d bytes to %s"), bytesWritten, filename); return true; } else { Log.errorln(F("PHApp::saveSignalPlotsToJson() - Failed to serialize JSON or write to file: %s"), filename); return false; } } #endif // ENABLE_PROFILE_SIGNAL_PLOT