250 lines
8.2 KiB
C++
250 lines
8.2 KiB
C++
#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<JsonObject>();
|
|
|
|
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<JsonObject>();
|
|
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<JsonObject>();
|
|
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<JsonObject>();
|
|
|
|
//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<const char *>() && strchr(fdo["ps"].as<const char *>(),'~') != strrchr(fdo["ps"].as<const char *>(),'~')))
|
|
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<const char*>()) strlcpy(saveName, sObj["n"].as<const char*>(), 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<const char*>()) strlcpy(quickLoad, sObj[F("ql")].as<const char*>(), 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<bool>() || sObj.size()==0 || index==255; // temporary preset needs brightness
|
|
segBounds = sObj["sb"].as<bool>() || sObj.size()==0 || index==255; // temporary preset needs bounds
|
|
selectedOnly = sObj[F("sc")].as<bool>();
|
|
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();
|
|
} |