#include "wled.h" /* * Methods to handle saving and loading presets to/from the filesystem */ #ifdef ARDUINO_ARCH_ESP32 static char *tmpRAMbuffer = nullptr; #endif static volatile byte presetToApply = 0; static volatile byte callModeToApply = 0; static volatile byte presetToSave = 0; static volatile int8_t saveLedmap = -1; static char quickLoad[9]; static char saveName[33]; static bool includeBri = true, segBounds = true, selectedOnly = false, playlistSave = false;; static const char *getFileName(bool persist = true) { return persist ? "/presets.json" : "/tmp.json"; } static void doSaveState() { bool persist = (presetToSave < 251); const char *filename = getFileName(persist); if (!requestJSONBufferLock(10)) return; // will set fileDoc initPresetsFile(); // just in case if someone deleted presets.json using /edit JsonObject sObj = doc.to(); DEBUG_PRINTLN(F("Serialize current state")); if (playlistSave) { serializePlaylist(sObj); if (includeBri) sObj["on"] = true; } else { serializeState(sObj, true, includeBri, segBounds, selectedOnly); } sObj["n"] = saveName; if (quickLoad[0]) sObj[F("ql")] = quickLoad; if (saveLedmap >= 0) sObj[F("ledmap")] = saveLedmap; /* #ifdef WLED_DEBUG DEBUG_PRINTLN(F("Serialized preset")); serializeJson(doc,Serial); DEBUG_PRINTLN(); #endif */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); size_t len = measureJson(*fileDoc) + 1; DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) if (psramFound()) tmpRAMbuffer = (char*) ps_malloc(len); else #endif tmpRAMbuffer = (char*) malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*fileDoc, tmpRAMbuffer, len); } else { writeObjectToFileUsingId(filename, presetToSave, fileDoc); } } else #endif writeObjectToFileUsingId(filename, presetToSave, fileDoc); if (persist) presetsModifiedTime = toki.second(); //unix time releaseJSONBufferLock(); updateFSInfo(); // clean up saveLedmap = -1; presetToSave = 0; saveName[0] = '\0'; quickLoad[0] = '\0'; playlistSave = false; } bool getPresetName(byte index, String& name) { if (!requestJSONBufferLock(9)) return false; bool presetExists = false; if (readObjectFromFileUsingId(getFileName(), index, &doc)) { JsonObject fdo = doc.as(); if (fdo["n"]) { name = (const char*)(fdo["n"]); presetExists = true; } } releaseJSONBufferLock(); return presetExists; } void initPresetsFile() { if (WLED_FS.exists(getFileName())) return; StaticJsonDocument<64> doc; JsonObject sObj = doc.to(); sObj.createNestedObject("0"); File f = WLED_FS.open(getFileName(), "w"); if (!f) { errorFlag = ERR_FS_GENERAL; return; } serializeJson(doc, f); f.close(); } bool applyPreset(byte index, byte callMode) { DEBUG_PRINT(F("Request to apply preset: ")); DEBUG_PRINTLN(index); presetToApply = index; callModeToApply = callMode; return true; } // apply preset or fallback to a effect and palette if it doesn't exist void applyPresetWithFallback(uint8_t index, uint8_t callMode, uint8_t effectID, uint8_t paletteID) { applyPreset(index, callMode); //these two will be overwritten if preset exists in handlePresets() effectCurrent = effectID; effectPalette = paletteID; } void handlePresets() { if (presetToSave) { doSaveState(); return; } if (presetToApply == 0 || fileDoc) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free bool changePreset = false; uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset() uint8_t tmpMode = callModeToApply; JsonObject fdo; const char *filename = getFileName(tmpPreset < 255); // allocate buffer if (!requestJSONBufferLock(9)) return; // will also assign fileDoc presetToApply = 0; //clear request for preset callModeToApply = 0; DEBUG_PRINT(F("Applying preset: ")); DEBUG_PRINTLN(tmpPreset); #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { deserializeJson(*fileDoc,tmpRAMbuffer); errorFlag = ERR_NONE; } else #endif { errorFlag = readObjectFromFileUsingId(filename, tmpPreset, fileDoc) ? ERR_NONE : ERR_FS_PLOAD; } fdo = fileDoc->as(); //HTTP API commands const char* httpwin = fdo["win"]; if (httpwin) { String apireq = "win"; // reduce flash string usage apireq += F("&IN&"); // internal call apireq += httpwin; handleSet(nullptr, apireq, false); // may call applyPreset() via PL= setValuesFromFirstSelectedSeg(); // fills legacy values changePreset = true; } else { if (!fdo["seg"].isNull() || !fdo["on"].isNull() || !fdo["bri"].isNull() || !fdo["nl"].isNull() || !fdo["ps"].isNull() || !fdo[F("playlist")].isNull()) changePreset = true; if (!(tmpMode == CALL_MODE_BUTTON_PRESET && fdo["ps"].is() && strchr(fdo["ps"].as(),'~') != strrchr(fdo["ps"].as(),'~'))) fdo.remove("ps"); // remove load request for presets to prevent recursive crash (if not called by button and contains preset cycling string "1~5~") deserializeState(fdo, CALL_MODE_NO_NOTIFY, tmpPreset); // may change presetToApply by calling applyPreset() } if (!errorFlag && tmpPreset < 255 && changePreset) currentPreset = tmpPreset; #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif releaseJSONBufferLock(); // will also clear fileDoc if (changePreset) notify(tmpMode); // force UDP notification stateUpdated(tmpMode); // was colorUpdated() if anything breaks updateInterfaces(tmpMode); } //called from handleSet(PS=) [network callback (fileDoc==nullptr), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { if (index == 0 || (index > 250 && index < 255)) return; if (pname) strlcpy(saveName, pname, 33); else { if (sObj["n"].is()) strlcpy(saveName, sObj["n"].as(), 33); else sprintf_P(saveName, PSTR("Preset %d"), index); } DEBUG_PRINT(F("Saving preset (")); DEBUG_PRINT(index); DEBUG_PRINT(F(") ")); DEBUG_PRINTLN(saveName); presetToSave = index; playlistSave = false; if (sObj[F("ql")].is()) strlcpy(quickLoad, sObj[F("ql")].as(), 9); // client limits QL to 2 chars, buffer for 8 bytes to allow unicode if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) includeBri = sObj["ib"].as() || sObj.size()==0 || index==255; // temporary preset needs brightness segBounds = sObj["sb"].as() || sObj.size()==0 || index==255; // temporary preset needs bounds selectedOnly = sObj[F("sc")].as(); saveLedmap = sObj[F("ledmap")] | -1; } else { // this is a playlist or API call if (sObj[F("playlist")].isNull()) { // we will save API call immediately (often causes presets.json corruption) presetToSave = 0; if (index > 250 || !fileDoc) return; // cannot save API calls to temporary preset (255) sObj.remove("o"); sObj.remove("v"); sObj.remove("time"); sObj.remove(F("error")); sObj.remove(F("psave")); if (sObj["n"].isNull()) sObj["n"] = saveName; initPresetsFile(); // just in case if someone deleted presets.json using /edit writeObjectToFileUsingId(getFileName(index<255), index, fileDoc); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } else { // store playlist // WARNING: playlist will be loaded in json.cpp after this call and will have repeat counter increased by 1 includeBri = true; // !sObj["on"].isNull(); playlistSave = true; } } } void deletePreset(byte index) { StaticJsonDocument<24> empty; writeObjectToFileUsingId(getFileName(), index, &empty); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); }