#include "wled.h" #include "palettes.h" #define JSON_PATH_STATE 1 #define JSON_PATH_INFO 2 #define JSON_PATH_STATE_INFO 3 #define JSON_PATH_NODES 4 #define JSON_PATH_PALETTES 5 #define JSON_PATH_FXDATA 6 #define JSON_PATH_NETWORKS 7 #define JSON_PATH_EFFECTS 8 /* * JSON API (De)serialization */ bool deserializeSegment(JsonObject elem, byte it, byte presetId) { byte id = elem["id"] | it; if (id >= strip.getMaxSegments()) return false; bool newSeg = false; int stop = elem["stop"] | -1; // append segment if (id >= strip.getSegmentsNum()) { if (stop <= 0) return false; // ignore empty/inactive segments strip.appendSegment(Segment(0, strip.getLengthTotal())); id = strip.getSegmentsNum()-1; // segments are added at the end of list newSeg = true; } //DEBUG_PRINTLN("-- JSON deserialize segment."); Segment& seg = strip.getSegment(id); //DEBUG_PRINTF("-- Original segment: %p (%p)\n", &seg, seg.data); Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) //DEBUG_PRINTF("-- Duplicate segment: %p (%p)\n", &prev, prev.data); uint16_t start = elem["start"] | seg.start; if (stop < 0) { int len = elem["len"]; stop = (len > 0) ? start + len : seg.stop; } // 2D segments uint16_t startY = elem["startY"] | seg.startY; uint16_t stopY = elem["stopY"] | seg.stopY; //repeat, multiplies segment until all LEDs are used, or max segments reached bool repeat = elem["rpt"] | false; if (repeat && stop>0) { elem.remove("id"); // remove for recursive call elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call uint16_t len = stop - start; for (size_t i=id+1; i= strip.getLengthTotal()) break; //TODO: add support for 2D elem["start"] = start; elem["stop"] = start + len; elem["rev"] = !elem["rev"]; // alternate reverse on even/odd segments deserializeSegment(elem, i, presetId); // recursive call with new id } return true; } if (elem["n"]) { // name field exists if (seg.name) { //clear old name delete[] seg.name; seg.name = nullptr; } const char * name = elem["n"].as(); size_t len = 0; if (name != nullptr) len = strlen(name); if (len > 0) { if (len > WLED_MAX_SEGNAME_LEN) len = WLED_MAX_SEGNAME_LEN; seg.name = new char[len+1]; if (seg.name) strlcpy(seg.name, name, WLED_MAX_SEGNAME_LEN+1); } else { // but is empty (already deleted above) elem.remove("n"); } } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field if (seg.name) { delete[] seg.name; seg.name = nullptr; } } uint16_t grp = elem["grp"] | seg.grouping; uint16_t spc = elem[F("spc")] | seg.spacing; uint16_t of = seg.offset; uint8_t soundSim = elem["si"] | seg.soundSim; uint8_t map1D2D = elem["m12"] | seg.map1D2D; if ((spc>0 && spc!=seg.spacing) || seg.map1D2D!=map1D2D) seg.fill(BLACK); // clear spacing gaps seg.map1D2D = constrain(map1D2D, 0, 7); seg.soundSim = constrain(soundSim, 0, 3); uint8_t set = elem[F("set")] | seg.set; seg.set = constrain(set, 0, 3); uint16_t len = 1; if (stop > start) len = stop - start; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { int offsetAbs = abs(offset); if (offsetAbs > len - 1) offsetAbs %= len; if (offset < 0) offsetAbs = len - offsetAbs; of = offsetAbs; } if (stop > start && of > len -1) of = len -1; // update segment (delete if necessary) // do not call seg.setUp() here, as it may cause a crash due to concurrent access if the segment is currently drawing effects // WS2812FX handles queueing of the change strip.setSegment(id, start, stop, grp, spc, of, startY, stopY); if (newSeg) seg.refreshLightCapabilities(); // fix for #3403 if (seg.reset && seg.stop == 0) { if (id == strip.getMainSegmentId()) strip.setMainSegmentId(0); // fix for #3403 return true; // segment was deleted & is marked for reset, no need to change anything else } byte segbri = seg.opacity; if (getVal(elem["bri"], &segbri)) { if (segbri > 0) seg.setOpacity(segbri); seg.setOption(SEG_OPTION_ON, segbri); // use transition } seg.setOption(SEG_OPTION_ON, getBoolVal(elem["on"], seg.on)); // use transition seg.freeze = getBoolVal(elem["frz"], seg.freeze); seg.setCCT(elem["cct"] | seg.cct); JsonArray colarr = elem["col"]; if (!colarr.isNull()) { if (seg.getLightCapabilities() & 3) { // segment has RGB or White for (size_t i = 0; i < 3; i++) { int rgbw[] = {0,0,0,0}; bool colValid = false; JsonArray colX = colarr[i]; if (colX.isNull()) { byte brgbw[] = {0,0,0,0}; const char* hexCol = colarr[i]; if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 int kelvin = colarr[i] | -1; if (kelvin < 0) continue; if (kelvin == 0) seg.setColor(i, 0); if (kelvin > 0) colorKtoRGB(kelvin, brgbw); colValid = true; } else { //HEX string, e.g. "FFAA00" colValid = colorFromHexString(brgbw, hexCol); } for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } else { //Array of ints (RGB or RGBW color), e.g. [255,160,0] byte sz = colX.size(); if (sz == 0) continue; //do nothing on empty array copyArray(colX, rgbw, 4); colValid = true; } if (!colValid) continue; seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) seg.setColor(0, ULTRAWHITE); seg.setColor(1, BLACK); } } // lx parser #ifdef WLED_ENABLE_LOXONE int lx = elem[F("lx")] | -1; if (lx > 0) { parseLxJson(lx, id, false); } int ly = elem[F("ly")] | -1; if (ly > 0) { parseLxJson(ly, id, true); } #endif #ifndef WLED_DISABLE_2D bool reverse = seg.reverse; bool mirror = seg.mirror; #endif seg.selected = getBoolVal(elem["sel"], seg.selected); seg.reverse = getBoolVal(elem["rev"], seg.reverse); seg.mirror = getBoolVal(elem["mi"] , seg.mirror); #ifndef WLED_DISABLE_2D bool reverse_y = seg.reverse_y; bool mirror_y = seg.mirror_y; seg.reverse_y = getBoolVal(elem["rY"] , seg.reverse_y); seg.mirror_y = getBoolVal(elem["mY"] , seg.mirror_y); seg.transpose = getBoolVal(elem[F("tp")], seg.transpose); if (seg.is2D() && seg.map1D2D == M12_pArc && (reverse != seg.reverse || reverse_y != seg.reverse_y || mirror != seg.mirror || mirror_y != seg.mirror_y)) seg.fill(BLACK); // clear entire segment (in case of Arc 1D to 2D expansion) #endif byte fx = seg.mode; if (getVal(elem["fx"], &fx, 0, strip.getModeCount())) { //load effect ('r' random, '~' inc/dec, 0-255 exact value) if (!presetId && currentPlaylist>=0) unloadPlaylist(); if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); } //getVal also supports inc/decrementing and random getVal(elem["sx"], &seg.speed); getVal(elem["ix"], &seg.intensity); uint8_t pal = seg.palette; if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments if (getVal(elem["pal"], &pal)) seg.setPalette(pal); } getVal(elem["c1"], &seg.custom1); getVal(elem["c2"], &seg.custom2); uint8_t cust3 = seg.custom3; getVal(elem["c3"], &cust3); // we can't pass reference to bifield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); seg.check2 = getBoolVal(elem["o2"], seg.check2); seg.check3 = getBoolVal(elem["o3"], seg.check3); JsonArray iarr = elem[F("i")]; //set individual LEDs if (!iarr.isNull()) { uint8_t oldMap1D2D = seg.map1D2D; seg.map1D2D = M12_Pixels; // no mapping // set brightness immediately and disable transition transitionDelayTemp = 0; jsonTransitionOnce = true; strip.setBrightness(scaledBri(bri), true); // freeze and init to black if (!seg.freeze) { seg.freeze = true; seg.fill(BLACK); } uint16_t start = 0, stop = 0; byte set = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { if(iarr[i].is()) { if (!set) { start = abs(iarr[i].as()); set++; } else { stop = abs(iarr[i].as()); set++; } } else { //color uint8_t rgbw[] = {0,0,0,0}; JsonArray icol = iarr[i]; if (!icol.isNull()) { //array, e.g. [255,0,0] byte sz = icol.size(); if (sz > 0 && sz < 5) copyArray(icol, rgbw); } else { //hex string, e.g. "FF0000" byte brgbw[] = {0,0,0,0}; const char* hexCol = iarr[i]; if (colorFromHexString(brgbw, hexCol)) { for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } } if (set < 2 || stop <= start) stop = start + 1; uint32_t c = gamma32(RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); while (start < stop) seg.setPixelColor(start++, c); set = 0; } } seg.map1D2D = oldMap1D2D; // restore mapping strip.trigger(); // force segment update } // send UDP/WS if segment options changed (except selection; will also deselect current preset) if (seg.differs(prev) & 0x7F) stateChanged = true; return true; } // deserializes WLED state (fileDoc points to doc object if called from web server) // presetId is non-0 if called from handlePreset() bool deserializeState(JsonObject root, byte callMode, byte presetId) { bool stateResponse = root[F("v")] | false; #if defined(WLED_DEBUG) && defined(WLED_DEBUG_HOST) netDebugEnabled = root[F("debug")] | netDebugEnabled; #endif bool onBefore = bri; getVal(root["bri"], &bri); bool on = root["on"] | (bri > 0); if (!on != !bri) toggleOnOff(); if (root["on"].is() && root["on"].as()[0] == 't') { if (onBefore || !bri) toggleOnOff(); // do not toggle off again if just turned on by bri (makes e.g. "{"on":"t","bri":32}" work) } if (bri && !onBefore) { // unfreeze all segments when turning on for (size_t s=0; s < strip.getSegmentsNum(); s++) { strip.getSegment(s).freeze = false; } if (realtimeMode && !realtimeOverride && useMainSegmentOnly) { // keep live segment frozen if live strip.getMainSegment().freeze = true; } } int tr = -1; if (!presetId || currentPlaylist < 0) { //do not apply transition time from preset if playlist active, as it would override playlist transition times tr = root[F("transition")] | -1; if (tr >= 0) { transitionDelay = tr; transitionDelay *= 100; transitionDelayTemp = transitionDelay; } } // temporary transition (applies only once) tr = root[F("tt")] | -1; if (tr >= 0) { transitionDelayTemp = tr; transitionDelayTemp *= 100; jsonTransitionOnce = true; } strip.setTransition(transitionDelayTemp); // required here for color transitions to have correct duration tr = root[F("tb")] | -1; if (tr >= 0) strip.timebase = ((uint32_t)tr) - millis(); JsonObject nl = root["nl"]; nightlightActive = getBoolVal(nl["on"], nightlightActive); nightlightDelayMins = nl["dur"] | nightlightDelayMins; nightlightMode = nl["mode"] | nightlightMode; nightlightTargetBri = nl[F("tbri")] | nightlightTargetBri; JsonObject udpn = root["udpn"]; sendNotificationsRT = getBoolVal(udpn["send"], sendNotificationsRT); syncGroups = udpn["sgrp"] | syncGroups; receiveGroups = udpn["rgrp"] | receiveGroups; if ((bool)udpn[F("nn")]) callMode = CALL_MODE_NO_NOTIFY; //send no notification just for this request unsigned long timein = root["time"] | UINT32_MAX; //backup time source if NTP not synced if (timein != UINT32_MAX) { setTimeFromAPI(timein); if (presetsModifiedTime == 0) presetsModifiedTime = timein; } if (root[F("psave")].isNull()) doReboot = root[F("rb")] | doReboot; // do not allow changing main segment while in realtime mode (may get odd results else) if (!realtimeMode) strip.setMainSegmentId(root[F("mainseg")] | strip.getMainSegmentId()); // must be before realtimeLock() if "live" realtimeOverride = root[F("lor")] | realtimeOverride; if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; } if (root.containsKey("live")) { if (root["live"].as()) { transitionDelayTemp = 0; jsonTransitionOnce = true; realtimeLock(65000); } else { exitRealtime(); } } int it = 0; JsonVariant segVar = root["seg"]; if (segVar.is()) { int id = segVar["id"] | -1; //if "seg" is not an array and ID not specified, apply to all selected/checked segments if (id < 0) { //apply all selected segments //bool didSet = false; for (size_t s = 0; s < strip.getSegmentsNum(); s++) { Segment &sg = strip.getSegment(s); if (sg.isSelected()) { deserializeSegment(segVar, s, presetId); //didSet = true; } } //TODO: not sure if it is good idea to change first active but unselected segment //if (!didSet) deserializeSegment(segVar, strip.getMainSegmentId(), presetId); } else { deserializeSegment(segVar, id, presetId); //apply only the segment with the specified ID } } else { size_t deleted = 0; JsonArray segs = segVar.as(); for (JsonObject elem : segs) { if (deserializeSegment(elem, it++, presetId) && !elem["stop"].isNull() && elem["stop"]==0) deleted++; } if (strip.getSegmentsNum() > 3 && deleted >= strip.getSegmentsNum()/2U) strip.purgeSegments(); // batch deleting more than half segments } usermods.readFromJsonState(root); loadLedmap = root[F("ledmap")] | loadLedmap; byte ps = root[F("psave")]; if (ps > 0 && ps < 251) savePreset(ps, nullptr, root); ps = root[F("pdel")]; //deletion if (ps > 0 && ps < 251) deletePreset(ps); // HTTP API commands (must be handled before "ps") const char* httpwin = root["win"]; if (httpwin) { String apireq = "win"; apireq += '&'; // reduce flash string usage apireq += httpwin; handleSet(nullptr, apireq, false); // may set stateChanged } // applying preset (2 cases: a) API call includes all preset values ("pd"), b) API only specifies preset ID ("ps")) byte presetToRestore = 0; // a) already applied preset content (requires "seg" or "win" but will ignore the rest) if (!root["pd"].isNull() && stateChanged) { currentPreset = root[F("pd")] | currentPreset; if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp] presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after //unloadPlaylist(); // applying a preset unloads the playlist, may be needed here too? } else if (!root["ps"].isNull()) { ps = presetCycCurr; if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) presetCycCurr = ps; unloadPlaylist(); // applying a preset unloads the playlist applyPreset(ps, callMode); // async load from file system (only preset ID was specified) return stateResponse; } } JsonObject playlist = root[F("playlist")]; if (!playlist.isNull() && loadPlaylist(playlist, presetId)) { //do not notify here, because the first playlist entry will do if (root["on"].isNull()) callMode = CALL_MODE_NO_NOTIFY; else callMode = CALL_MODE_DIRECT_CHANGE; // possible bugfix for playlist only containing HTTP API preset FX=~ } if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { if (strip.customPalettes.size()) { char fileName[32]; sprintf_P(fileName, PSTR("/palette%d.json"), strip.customPalettes.size()-1); if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); strip.loadCustomPalettes(); } } stateUpdated(callMode); if (presetToRestore) currentPreset = presetToRestore; return stateResponse; } void serializeSegment(JsonObject& root, Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { root["start"] = seg.start; root["stop"] = seg.stop; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { root[F("startY")] = seg.startY; root[F("stopY")] = seg.stopY; } #endif } if (!forPreset) root["len"] = seg.stop - seg.start; root["grp"] = seg.grouping; root[F("spc")] = seg.spacing; root[F("of")] = seg.offset; root["on"] = seg.on; root["frz"] = seg.freeze; byte segbri = seg.opacity; root["bri"] = (segbri) ? segbri : 255; root["cct"] = seg.cct; root[F("set")] = seg.set; if (seg.name != nullptr) root["n"] = reinterpret_cast(seg.name); //not good practice, but decreases required JSON buffer else if (forPreset) root["n"] = ""; // to conserve RAM we will serialize the col array manually // this will reduce RAM footprint from ~300 bytes to 84 bytes per segment char colstr[70]; colstr[0] = '['; colstr[1] = '\0'; //max len 68 (5 chan, all 255) const char *format = strip.hasWhiteChannel() ? PSTR("[%u,%u,%u,%u]") : PSTR("[%u,%u,%u]"); for (size_t i = 0; i < 3; i++) { byte segcol[4]; byte* c = segcol; segcol[0] = R(seg.colors[i]); segcol[1] = G(seg.colors[i]); segcol[2] = B(seg.colors[i]); segcol[3] = W(seg.colors[i]); char tmpcol[22]; sprintf_P(tmpcol, format, (unsigned)c[0], (unsigned)c[1], (unsigned)c[2], (unsigned)c[3]); strcat(colstr, i<2 ? strcat(tmpcol, ",") : tmpcol); } strcat(colstr, "]"); root["col"] = serialized(colstr); root["fx"] = seg.mode; root["sx"] = seg.speed; root["ix"] = seg.intensity; root["pal"] = seg.palette; root["c1"] = seg.custom1; root["c2"] = seg.custom2; root["c3"] = seg.custom3; root["sel"] = seg.isSelected(); root["rev"] = seg.reverse; root["mi"] = seg.mirror; #ifndef WLED_DISABLE_2D if (strip.isMatrix) { root["rY"] = seg.reverse_y; root["mY"] = seg.mirror_y; root[F("tp")] = seg.transpose; } #endif root["o1"] = seg.check1; root["o2"] = seg.check2; root["o3"] = seg.check3; root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) { if (includeBri) { root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms } if (!forPreset) { if (errorFlag) {root[F("error")] = errorFlag; errorFlag = ERR_NONE;} //prevent error message to persist on screen root["ps"] = (currentPreset > 0) ? currentPreset : -1; root[F("pl")] = currentPlaylist; usermods.addToJsonState(root); JsonObject nl = root.createNestedObject("nl"); nl["on"] = nightlightActive; nl["dur"] = nightlightDelayMins; nl["mode"] = nightlightMode; nl[F("tbri")] = nightlightTargetBri; if (nightlightActive) { nl[F("rem")] = (nightlightDelayMs - (millis() - nightlightStartTime)) / 1000; // seconds remaining } else { nl[F("rem")] = -1; } JsonObject udpn = root.createNestedObject("udpn"); udpn["send"] = sendNotificationsRT; udpn["recv"] = receiveGroups != 0; udpn["sgrp"] = syncGroups; udpn["rgrp"] = receiveGroups; root[F("lor")] = realtimeOverride; } root[F("mainseg")] = strip.getMainSegmentId(); JsonArray seg = root.createNestedArray("seg"); for (size_t s = 0; s < strip.getMaxSegments(); s++) { if (s >= strip.getSegmentsNum()) { if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); seg0["stop"] = 0; continue; } else break; } Segment &sg = strip.getSegment(s); if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue; if (sg.isActive()) { JsonObject seg0 = seg.createNestedObject(); serializeSegment(seg0, sg, s, forPreset, segmentBounds); } else if (forPreset && segmentBounds) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); seg0["stop"] = 0; } } } void serializeInfo(JsonObject root) { root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); JsonObject leds = root.createNestedObject("leds"); leds[F("count")] = strip.getLengthTotal(); leds[F("pwr")] = strip.currentMilliamps; leds["fps"] = strip.getFps(); leds[F("maxpwr")] = (strip.currentMilliamps)? strip.ablMilliampsMax : 0; leds[F("maxseg")] = strip.getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config #ifndef WLED_DISABLE_2D if (strip.isMatrix) { JsonObject matrix = leds.createNestedObject("matrix"); matrix["w"] = Segment::maxWidth; matrix["h"] = Segment::maxHeight; } #endif uint8_t totalLC = 0; JsonArray lcarr = leds.createNestedArray(F("seglc")); size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; uint8_t lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; lcarr.add(lc); } leds["lc"] = totalLC; leds[F("rgbw")] = strip.hasRGBWBus(); // deprecated, use info.leds.lc leds[F("wv")] = totalLC & 0x02; // deprecated, true if white slider should be displayed for any segment leds["cct"] = totalLC & 0x04; // deprecated, use info.leds.lc #ifdef WLED_DEBUG JsonArray i2c = root.createNestedArray(F("i2c")); i2c.add(i2c_sda); i2c.add(i2c_scl); JsonArray spi = root.createNestedArray(F("spi")); spi.add(spi_mosi); spi.add(spi_sclk); spi.add(spi_miso); #endif root[F("str")] = false; //syncToggleReceive; root[F("name")] = serverDescription; root[F("udpport")] = udpPort; root["live"] = (bool)realtimeMode; root[F("liveseg")] = useMainSegmentOnly ? strip.getMainSegmentId() : -1; // if using main segment only for live switch (realtimeMode) { case REALTIME_MODE_INACTIVE: root["lm"] = ""; break; case REALTIME_MODE_GENERIC: root["lm"] = ""; break; case REALTIME_MODE_UDP: root["lm"] = F("UDP"); break; case REALTIME_MODE_HYPERION: root["lm"] = F("Hyperion"); break; case REALTIME_MODE_E131: root["lm"] = F("E1.31"); break; case REALTIME_MODE_ADALIGHT: root["lm"] = F("USB Adalight/TPM2"); break; case REALTIME_MODE_ARTNET: root["lm"] = F("Art-Net"); break; case REALTIME_MODE_TPM2NET: root["lm"] = F("tpm2.net"); break; case REALTIME_MODE_DDP: root["lm"] = F("DDP"); break; } if (realtimeIP[0] == 0) { root[F("lip")] = ""; } else { root[F("lip")] = realtimeIP.toString(); } #ifdef WLED_ENABLE_WEBSOCKETS root[F("ws")] = ws.count(); #else root[F("ws")] = -1; #endif root[F("fxcount")] = strip.getModeCount(); root[F("palcount")] = strip.getPaletteCount(); root[F("cpalcount")] = strip.customPalettes.size(); //number of custom palettes JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; i>i) & 0x00000001U) { JsonObject ledmaps0 = ledmaps.createNestedObject(); ledmaps0["id"] = i; #ifndef ESP8266 if (i && ledmapNames[i-1]) ledmaps0["n"] = ledmapNames[i-1]; #endif } } JsonObject wifi_info = root.createNestedObject("wifi"); wifi_info[F("bssid")] = WiFi.BSSIDstr(); int qrssi = WiFi.RSSI(); wifi_info[F("rssi")] = qrssi; wifi_info[F("signal")] = getSignalQuality(qrssi); wifi_info[F("channel")] = WiFi.channel(); JsonObject fs_info = root.createNestedObject("fs"); fs_info["u"] = fsBytesUsed / 1000; fs_info["t"] = fsBytesTotal / 1000; fs_info[F("pmt")] = presetsModifiedTime; root[F("ndc")] = nodeListEnabled ? (int)Nodes.size() : -1; #ifdef ARDUINO_ARCH_ESP32 #ifdef WLED_DEBUG wifi_info[F("txPower")] = (int) WiFi.getTxPower(); wifi_info[F("sleep")] = (bool) WiFi.getSleep(); #endif #if !defined(CONFIG_IDF_TARGET_ESP32C2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) root[F("arch")] = "esp32"; #else root[F("arch")] = ESP.getChipModel(); #endif root[F("core")] = ESP.getSdkVersion(); //root[F("maxalloc")] = ESP.getMaxAllocHeap(); #ifdef WLED_DEBUG root[F("resetReason0")] = (int)rtc_get_reset_reason(0); root[F("resetReason1")] = (int)rtc_get_reset_reason(1); #endif root[F("lwip")] = 0; //deprecated #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); //root[F("maxalloc")] = ESP.getMaxFreeBlockSize(); #ifdef WLED_DEBUG root[F("resetReason")] = (int)ESP.getResetInfoPtr()->reason; #endif root[F("lwip")] = LWIP_VERSION_MAJOR; #endif root[F("freeheap")] = ESP.getFreeHeap(); #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) if (psramFound()) root[F("psram")] = ESP.getFreePsram(); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; char time[32]; getTimeString(time); root[F("time")] = time; usermods.addToJsonInfo(root); uint16_t os = 0; #ifdef WLED_DEBUG os = 0x80; #ifdef WLED_DEBUG_HOST os |= 0x0100; if (!netDebugEnabled) os &= ~0x0080; #endif #endif #ifndef WLED_DISABLE_ALEXA os += 0x40; #endif //os += 0x20; // indicated now removed Blynk support, may be reused to indicate another build-time option #ifdef USERMOD_CRONIXIE os += 0x10; #endif #ifndef WLED_DISABLE_FILESYSTEM os += 0x08; #endif #ifndef WLED_DISABLE_HUESYNC os += 0x04; #endif #ifdef WLED_ENABLE_ADALIGHT os += 0x02; #endif #ifndef WLED_DISABLE_OTA os += 0x01; #endif root[F("opt")] = os; root[F("brand")] = "WLED"; root[F("product")] = F("FOSS"); root["mac"] = escapedMac; char s[16] = ""; if (Network.isConnected()) { IPAddress localIP = Network.localIP(); sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); } root["ip"] = s; } void setPaletteColors(JsonArray json, CRGBPalette16 palette) { for (int i = 0; i < 16; i++) { JsonArray colors = json.createNestedArray(); CRGB color = palette[i]; colors.add(i<<4); colors.add(color.red); colors.add(color.green); colors.add(color.blue); } } void setPaletteColors(JsonArray json, byte* tcp) { TRGBGradientPaletteEntryUnion* ent = (TRGBGradientPaletteEntryUnion*)(tcp); TRGBGradientPaletteEntryUnion u; // Count entries uint16_t count = 0; do { u = *(ent + count); count++; } while ( u.index != 255); u = *ent; int indexstart = 0; while( indexstart < 255) { indexstart = u.index; JsonArray colors = json.createNestedArray(); colors.add(u.index); colors.add(u.r); colors.add(u.g); colors.add(u.b); ent++; u = *ent; } } void serializePalettes(JsonObject root, int page) { byte tcp[72]; #ifdef ESP8266 int itemPerPage = 5; #else int itemPerPage = 8; #endif int palettesCount = strip.getPaletteCount(); int customPalettes = strip.customPalettes.size(); int maxPage = (palettesCount + customPalettes -1) / itemPerPage; if (page > maxPage) page = maxPage; int start = itemPerPage * page; int end = start + itemPerPage; if (end > palettesCount + customPalettes) end = palettesCount + customPalettes; root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); for (int i = start; i < end; i++) { JsonArray curPalette = palettes.createNestedArray(String(i>=palettesCount ? 255 - i + palettesCount : i)); switch (i) { case 0: //default palette setPaletteColors(curPalette, PartyColors_p); break; case 1: //random curPalette.add("r"); curPalette.add("r"); curPalette.add("r"); curPalette.add("r"); break; case 2: //primary color only curPalette.add("c1"); break; case 3: //primary + secondary curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c2"); curPalette.add("c2"); break; case 4: //primary + secondary + tertiary curPalette.add("c3"); curPalette.add("c2"); curPalette.add("c1"); break; case 5: //primary + secondary (+tert if not off), more distinct curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c1"); curPalette.add("c2"); curPalette.add("c2"); curPalette.add("c2"); curPalette.add("c2"); curPalette.add("c2"); curPalette.add("c3"); curPalette.add("c3"); curPalette.add("c3"); curPalette.add("c3"); curPalette.add("c3"); curPalette.add("c1"); break; case 6: //Party colors setPaletteColors(curPalette, PartyColors_p); break; case 7: //Cloud colors setPaletteColors(curPalette, CloudColors_p); break; case 8: //Lava colors setPaletteColors(curPalette, LavaColors_p); break; case 9: //Ocean colors setPaletteColors(curPalette, OceanColors_p); break; case 10: //Forest colors setPaletteColors(curPalette, ForestColors_p); break; case 11: //Rainbow colors setPaletteColors(curPalette, RainbowColors_p); break; case 12: //Rainbow stripe colors setPaletteColors(curPalette, RainbowStripeColors_p); break; default: { if (i>=palettesCount) { setPaletteColors(curPalette, strip.customPalettes[i - palettesCount]); } else { memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[i - 13])), 72); setPaletteColors(curPalette, tcp); } } break; } } } void serializeNetworks(JsonObject root) { JsonArray networks = root.createNestedArray(F("networks")); int16_t status = WiFi.scanComplete(); switch (status) { case WIFI_SCAN_FAILED: WiFi.scanNetworks(true); return; case WIFI_SCAN_RUNNING: return; } for (int i = 0; i < status; i++) { JsonObject node = networks.createNestedObject(); node["ssid"] = WiFi.SSID(i); node["rssi"] = WiFi.RSSI(i); node["bssid"] = WiFi.BSSIDstr(i); node["channel"] = WiFi.channel(i); node["enc"] = WiFi.encryptionType(i); } WiFi.scanDelete(); if (WiFi.scanComplete() == WIFI_SCAN_FAILED) { WiFi.scanNetworks(true); } } void serializeNodes(JsonObject root) { JsonArray nodes = root.createNestedArray("nodes"); for (NodesMap::iterator it = Nodes.begin(); it != Nodes.end(); ++it) { if (it->second.ip[0] != 0) { JsonObject node = nodes.createNestedObject(); node[F("name")] = it->second.nodeName; node["type"] = it->second.nodeType; node["ip"] = it->second.ip.toString(); node[F("age")] = it->second.age; node[F("vid")] = it->second.build; } } } // deserializes mode data string into JsonArray void serializeModeData(JsonArray fxdata) { char lineBuffer[256]; for (size_t i = 0; i < strip.getModeCount(); i++) { strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string if (lineBuffer[0] != 0) { char* dataPtr = strchr(lineBuffer,'@'); if (dataPtr) fxdata.add(dataPtr+1); else fxdata.add(""); } } } // deserializes mode names string into JsonArray // also removes effect data extensions (@...) from deserialised names void serializeModeNames(JsonArray arr) { char lineBuffer[256]; for (size_t i = 0; i < strip.getModeCount(); i++) { strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string if (lineBuffer[0] != 0) { char* dataPtr = strchr(lineBuffer,'@'); if (dataPtr) *dataPtr = 0; // terminate mode data after name arr.add(lineBuffer); } } } static volatile bool servingClient = false; void serveJson(AsyncWebServerRequest* request) { if (servingClient) { request->send(503, "application/json", F("{\"error\":2}")); // ERR_CONCURENCY return; } servingClient = true; byte subJson = 0; const String& url = request->url(); if (url.indexOf("state") > 0) subJson = JSON_PATH_STATE; else if (url.indexOf("info") > 0) subJson = JSON_PATH_INFO; else if (url.indexOf("si") > 0) subJson = JSON_PATH_STATE_INFO; else if (url.indexOf("nodes") > 0) subJson = JSON_PATH_NODES; else if (url.indexOf("eff") > 0) subJson = JSON_PATH_EFFECTS; else if (url.indexOf("palx") > 0) subJson = JSON_PATH_PALETTES; else if (url.indexOf("fxda") > 0) subJson = JSON_PATH_FXDATA; else if (url.indexOf("net") > 0) subJson = JSON_PATH_NETWORKS; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); servingClient = false; return; } #endif else if (url.indexOf("pal") > 0) { request->send_P(200, "application/json", JSON_palette_names); servingClient = false; return; } else if (url.indexOf("cfg") > 0 && handleFileRead(request, "/cfg.json")) { servingClient = false; return; } else if (url.length() > 6) { //not just /json request->send(501, "application/json", F("{\"error\":\"Not implemented\"}")); servingClient = false; return; } if (!requestJSONBufferLock(17)) { request->send(503, "application/json", F("{\"error\":3}")); servingClient = false; return; } AsyncJsonResponse *response = new AsyncJsonResponse(&doc, subJson==JSON_PATH_FXDATA || subJson==JSON_PATH_EFFECTS); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); switch (subJson) { case JSON_PATH_STATE: serializeState(lDoc); break; case JSON_PATH_INFO: serializeInfo(lDoc); break; case JSON_PATH_NODES: serializeNodes(lDoc); break; case JSON_PATH_PALETTES: serializePalettes(lDoc, request->hasParam("page") ? request->getParam("page")->value().toInt() : 0); break; case JSON_PATH_EFFECTS: serializeModeNames(lDoc); break; case JSON_PATH_FXDATA: serializeModeData(lDoc); break; case JSON_PATH_NETWORKS: serializeNetworks(lDoc); break; default: //all JsonObject state = lDoc.createNestedObject("state"); serializeState(state); JsonObject info = lDoc.createNestedObject("info"); serializeInfo(info); if (subJson != JSON_PATH_STATE_INFO) { JsonArray effects = lDoc.createNestedArray(F("effects")); serializeModeNames(effects); // remove WLED-SR extensions from effect names lDoc[F("palettes")] = serialized((const __FlashStringHelper*)JSON_palette_names); } //lDoc["m"] = lDoc.memoryUsage(); // JSON buffer usage, for remote debugging } DEBUG_PRINTF("JSON buffer size: %u for request: %d\n", lDoc.memoryUsage(), subJson); #ifdef WLED_DEBUG size_t len = #endif response->setLength(); DEBUG_PRINT(F("JSON content length: ")); DEBUG_PRINTLN(len); request->send(response); releaseJSONBufferLock(); servingClient = false; } #ifdef WLED_ENABLE_JSONLIVE #define MAX_LIVE_LEDS 256 bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) { #ifdef WLED_ENABLE_WEBSOCKETS AsyncWebSocketClient * wsc = nullptr; if (!request) { //not HTTP, use Websockets wsc = ws.client(wsClient); if (!wsc || wsc->queueLength() > 0) return false; //only send if queue free } #endif uint16_t used = strip.getLengthTotal(); uint16_t n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS #ifndef WLED_DISABLE_2D if (strip.isMatrix) { // ignore anything behid matrix (i.e. extra strip) used = Segment::maxWidth*Segment::maxHeight; // always the size of matrix (more or less than strip.getLengthTotal()) n = 1; if (used > MAX_LIVE_LEDS) n = 2; if (used > MAX_LIVE_LEDS*4) n = 4; } #endif char buffer[2048]; // shoud be enough for 256 LEDs [RRGGBB] + all other text (9+25) strcpy_P(buffer, PSTR("{\"leds\":[")); obuf = buffer; // assign buffer for oappnd() functions olen = 9; for (size_t i = 0; i < used; i += n) { #ifndef WLED_DISABLE_2D if (strip.isMatrix && n>1 && (i/Segment::maxWidth)%n) i += Segment::maxWidth * (n-1); #endif uint32_t c = strip.getPixelColor(i); uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map g = scale8(qadd8(w, g), strip.getBrightness()); //G b = scale8(qadd8(w, b), strip.getBrightness()); //B olen += sprintf_P(obuf + olen, PSTR("\"%06X\","), RGBW32(r,g,b,0)); } olen -= 1; oappend((const char*)F("],\"n\":")); oappendi(n); #ifndef WLED_DISABLE_2D if (strip.isMatrix) { oappend((const char*)F(",\"w\":")); oappendi(Segment::maxWidth/n); oappend((const char*)F(",\"h\":")); oappendi(Segment::maxHeight/n); } #endif oappend("}"); if (request) { request->send(200, "application/json", buffer); } #ifdef WLED_ENABLE_WEBSOCKETS else { wsc->text(obuf, olen); } #endif obuf = nullptr; return true; } #endif