#include "wled.h"
#include "wled_ethernet.h"

/*
 * Serializes and parses the cfg.json and wsec.json settings files, stored in internal FS.
 * The structure of the JSON is not to be considered an official API and may change without notice.
 */

#ifndef PIXEL_COUNTS
  #define PIXEL_COUNTS DEFAULT_LED_COUNT
#endif

#ifndef DATA_PINS
  #define DATA_PINS DEFAULT_LED_PIN
#endif

#ifndef LED_TYPES
  #define LED_TYPES DEFAULT_LED_TYPE
#endif

#ifndef DEFAULT_LED_COLOR_ORDER
  #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB  //default to GRB
#endif

static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) {
  return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0;
}

static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) {
  // Pins provided < pins required -> always invalid
  // Pins provided = pins required -> always valid
  // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated
  return (sumPinsRequired(types, numTypes) > numPins) ? false :
          (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0;
}


//simple macro for ArduinoJSON's or syntax
#define CJSON(a,b) a = b | a

static inline void getStringFromJson(char* dest, const char* src, size_t len) {
  if (src != nullptr) strlcpy(dest, src, len);
}

bool deserializeConfig(JsonObject doc, bool fromFS) {
  bool needsSave = false;
  //int rev_major = doc["rev"][0]; // 1
  //int rev_minor = doc["rev"][1]; // 0

  //long vid = doc[F("vid")]; // 2010020

  JsonObject id = doc["id"];
  getStringFromJson(cmDNS, id[F("mdns")], 33);
  getStringFromJson(serverDescription, id[F("name")], 33);
#ifndef WLED_DISABLE_ALEXA
  getStringFromJson(alexaInvocationName, id[F("inv")], 33);
#endif
  CJSON(simplifiedUI, id[F("sui")]);

  JsonObject nw = doc["nw"];
#ifndef WLED_DISABLE_ESPNOW
  CJSON(enableESPNow, nw[F("espnow")]);
  linked_remotes.clear();
  JsonVariant lrem = nw[F("linked_remote")];
  if (!lrem.isNull()) {
     if (lrem.is<JsonArray>()) {
      for (size_t i = 0; i < lrem.size(); i++) {
        std::array<char, 13> entry{};
        getStringFromJson(entry.data(), lrem[i], 13);
        entry[12] = '\0';
        linked_remotes.emplace_back(entry);
      }
    }
    else { // legacy support for single MAC address in config
      std::array<char, 13> entry{};
      getStringFromJson(entry.data(), lrem, 13);
      entry[12] = '\0';
      linked_remotes.emplace_back(entry);
    }
  }
#endif

  size_t n = 0;
  JsonArray nw_ins = nw["ins"];
  if (!nw_ins.isNull()) {
    // as password are stored separately in wsec.json when reading configuration vector resize happens there, but for dynamic config we need to resize if necessary
    size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size());               // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
    if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
    for (JsonObject wifi : nw_ins) {
      JsonArray ip = wifi["ip"];
      JsonArray gw = wifi["gw"];
      JsonArray sn = wifi["sn"];
      char ssid[33] = "";
      char pass[65] = "";
      char bssid[13] = "";
      IPAddress nIP = (uint32_t)0U, nGW = (uint32_t)0U, nSN = (uint32_t)0x00FFFFFF; // little endian
      getStringFromJson(ssid, wifi[F("ssid")], 33);
      getStringFromJson(pass, wifi["psk"], 65); // password is not normally present but if it is, use it
      getStringFromJson(bssid, wifi[F("bssid")], 13);
      for (size_t i = 0; i < 4; i++) {
        CJSON(nIP[i], ip[i]);
        CJSON(nGW[i], gw[i]);
        CJSON(nSN[i], sn[i]);
      }
      if (strlen(ssid) > 0) strlcpy(multiWiFi[n].clientSSID, ssid, 33); // this will keep old SSID intact if not present in JSON
      if (strlen(pass) > 0) strlcpy(multiWiFi[n].clientPass, pass, 65); // this will keep old password intact if not present in JSON
      if (strlen(bssid) > 0) fillStr2MAC(multiWiFi[n].bssid, bssid);
      multiWiFi[n].staticIP = nIP;
      multiWiFi[n].staticGW = nGW;
      multiWiFi[n].staticSN = nSN;
#ifdef WLED_ENABLE_WPA_ENTERPRISE
      byte encType = WIFI_ENCRYPTION_TYPE_PSK;
      char anonIdent[65] = "";
      char ident[65] = "";
      CJSON(encType, wifi[F("enc_type")]);
      getStringFromJson(anonIdent, wifi["e_anon_ident"], 65);
      getStringFromJson(ident, wifi["e_ident"], 65);
      multiWiFi[n].encryptionType = encType;
      strlcpy(multiWiFi[n].enterpriseAnonIdentity, anonIdent, 65);
      strlcpy(multiWiFi[n].enterpriseIdentity, ident, 65);
#endif
      if (++n >= WLED_MAX_WIFI_COUNT) break;
    }
  }

  JsonArray dns = nw[F("dns")];
  if (!dns.isNull()) {
    for (size_t i = 0; i < 4; i++) {
      CJSON(dnsAddress[i], dns[i]);
    }
  }

  // https://github.com/wled/WLED/issues/5247
#ifdef WLED_USE_ETHERNET
  JsonObject ethernet = doc[F("eth")];
  CJSON(ethernetType, ethernet["type"]);
  // NOTE: Ethernet configuration takes priority over other use of pins
  initEthernet();
#endif

  JsonObject ap = doc["ap"];
  getStringFromJson(apSSID, ap[F("ssid")], 33);
  getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security
  //int ap_pskl = ap[F("pskl")];
  CJSON(apChannel, ap[F("chan")]);
  if (apChannel > 13 || apChannel < 1) apChannel = 6; // reset to default if invalid
  CJSON(apHide, ap[F("hide")]);
  if (apHide > 1) apHide = 1;
  CJSON(apBehavior, ap[F("behav")]);
  /*
  JsonArray ap_ip = ap["ip"];
  for (unsigned i = 0; i < 4; i++) {
    apIP[i] = ap_ip;
  }
  */

  JsonObject wifi = doc[F("wifi")];
  noWifiSleep = !(wifi[F("sleep")] | !noWifiSleep); // inverted
  //noWifiSleep = !noWifiSleep;
  CJSON(force802_3g, wifi[F("phy")]); //force phy mode g?
#ifdef ARDUINO_ARCH_ESP32
  CJSON(txPower, wifi[F("txpwr")]);
  txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm);
#endif

  JsonObject hw = doc[F("hw")];

  // initialize LED pins and lengths prior to other HW (except for ethernet)
  JsonObject hw_led = hw["led"];

  uint16_t total = hw_led[F("total")] | strip.getLengthTotal();
  uint16_t ablMilliampsMax = hw_led[F("maxpwr")] | BusManager::ablMilliampsMax();
  BusManager::setMilliampsMax(ablMilliampsMax);
  Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED);
  CJSON(strip.correctWB, hw_led["cct"]);
  CJSON(strip.cctFromRgb, hw_led[F("cr")]);
  CJSON(cctICused, hw_led[F("ic")]);
  uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend();
  Bus::setCCTBlend(cctBlending);
  unsigned targetFPS = hw_led["fps"] | WLED_FPS;
  strip.setTargetFps(targetFPS); //unlimited if 0, default 42 FPS

  #ifndef WLED_DISABLE_2D
  // 2D Matrix Settings
  JsonObject matrix = hw_led[F("matrix")];
  if (!matrix.isNull()) {
    strip.isMatrix = true;
    unsigned numPanels = matrix[F("mpc")] | 1;
    numPanels = constrain(numPanels, 1, WLED_MAX_PANELS);
    strip.panel.clear();
    JsonArray panels = matrix[F("panels")];
    unsigned s = 0;
    if (!panels.isNull()) {
      strip.panel.reserve(numPanels);  // pre-allocate default 8x8 panels
      for (JsonObject pnl : panels) {
        WS2812FX::Panel p;
        CJSON(p.bottomStart, pnl["b"]);
        CJSON(p.rightStart,  pnl["r"]);
        CJSON(p.vertical,    pnl["v"]);
        CJSON(p.serpentine,  pnl["s"]);
        CJSON(p.xOffset,     pnl["x"]);
        CJSON(p.yOffset,     pnl["y"]);
        CJSON(p.height,      pnl["h"]);
        CJSON(p.width,       pnl["w"]);
        strip.panel.push_back(p);
        if (++s >= numPanels) break; // max panels reached
      }
    }
    strip.panel.shrink_to_fit();  // release unused memory (just in case)
    // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer
    //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix
  }
  #endif

  DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), getFreeHeapSize());
  JsonArray ins = hw_led["ins"];
  if (!ins.isNull()) {
    int s = 0;  // bus iterator
    for (JsonObject elm : ins) {
      if (s >= WLED_MAX_BUSSES) break; // only counts physical buses
      uint8_t pins[OUTPUT_MAX_PINS] = {255, 255, 255, 255, 255};
      JsonArray pinArr = elm["pin"];
      if (pinArr.size() == 0) continue;
      //pins[0] = pinArr[0];
      unsigned i = 0;
      for (int p : pinArr) {
        pins[i++] = p;
        if (i>4) break;
      }
      uint16_t length = elm["len"] | 1;
      uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble
      uint8_t skipFirst = elm[F("skip")];
      uint16_t start = elm["start"] | 0;
      if (length==0 || start + length > MAX_LEDS) continue; // zero length or we reached max. number of LEDs, just stop
      uint8_t ledType = elm["type"] | TYPE_WS2812_RGB;
      bool reversed = elm["rev"];
      bool refresh = elm["ref"] | false;
      uint16_t freqkHz = elm[F("freq")] | 0;  // will be in kHz for DotStar and Hz for PWM
      uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY;
      uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT;
      uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists
      // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current)
      if (Bus::isPWM(ledType) || Bus::isOnOff(ledType) || Bus::isVirtual(ledType)) { // analog and virtual
        maPerLed = 0;
        maMax = 0;
      }
      ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh
      uint8_t driverType = elm[F("drv")] | 0; // 0=RMT (default), 1=I2S note: polybus may override this if driver is not available

      String host = elm[F("text")] | String();
      busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax, driverType, host);
      doInitBusses = true;  // finalization done in beginStrip()
      if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want
    }
  } else if (fromFS) {
    //if busses failed to load, add default (fresh install, FS issue, ...)
    BusManager::removeAll();
    busConfigs.clear();

    DEBUG_PRINTLN(F("No busses, init default"));
    constexpr unsigned defDataTypes[] = {LED_TYPES};
    constexpr unsigned defDataPins[] = {DATA_PINS};
    constexpr unsigned defCounts[] = {PIXEL_COUNTS};
    constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0]));
    constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0]));
    constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0]));

    static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins),
                  "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES");

    unsigned pinsIndex = 0;
    for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) {
      uint8_t defPin[OUTPUT_MAX_PINS];
      // if we have less types than requested outputs and they do not align, use last known type to set current type
      unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1];
      unsigned busPins = Bus::getNumberOfPins(dataType);

      // if we need more pins than available all outputs have been configured
      if (pinsIndex + busPins > defNumPins) break;

      // Assign all pins first so we can check for conflicts on this bus
      for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j];

      for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) {
        bool validPin = true;
        // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware
        // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc.
        // Pin should not be already allocated, read/only or defined for current bus
        while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) {
          if (validPin) {
            DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output."));
            defPin[j] = 1; // start with GPIO1 and work upwards
            validPin = false;
          } else if (defPin[j] < WLED_NUM_PINS) {
            defPin[j]++;
          } else {
            DEBUG_PRINTLN(F("No available pins left! Can't configure output."));
            break;
          }
          // is the newly assigned pin already defined or used previously?
          // try next in line until there are no clashes or we run out of pins
          bool clash;
          do {
            clash = false;
            // check for conflicts on current bus
            for (const auto &pin : defPin) {
              if (&pin != &defPin[j] && pin == defPin[j]) {
                clash = true;
                break;
              }
            }
            // We already have a clash on current bus, no point checking next buses
            if (!clash) {
              // check for conflicts in defined pins
              for (const auto &pin : defDataPins) {
                if (pin == defPin[j]) {
                  clash = true;
                  break;
                }
              }
            }
            if (clash) defPin[j]++;
            if (defPin[j] >= WLED_NUM_PINS) break;
          } while (clash);
        }
      }
      pinsIndex += busPins;

      // if we have less counts than pins and they do not align, use last known count to set current count
      unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1];
      unsigned start = 0;
      // analog always has length 1
      if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;
      busConfigs.emplace_back(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, LED_MILLIAMPS_DEFAULT, ABL_MILLIAMPS_DEFAULT, 0); // driver=0 (RMT default)
      doInitBusses = true;  // finalization done in beginStrip()
    }
  }
  if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus

  // read color order map configuration
  JsonArray hw_com = hw[F("com")];
  if (!hw_com.isNull()) {
    BusManager::getColorOrderMap().reserve(std::min(hw_com.size(), (size_t)WLED_MAX_COLOR_ORDER_MAPPINGS));
    for (JsonObject entry : hw_com) {
      uint16_t start = entry["start"] | 0;
      uint16_t len = entry["len"] | 0;
      uint8_t colorOrder = (int)entry[F("order")];
      if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break;
    }
  }

  // read multiple button configuration
  JsonObject btn_obj = hw["btn"];
  CJSON(touchThreshold, btn_obj[F("tt")]);
  bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled
  disablePullUp = !pull;
  JsonArray hw_btn_ins = btn_obj["ins"];
  if (!hw_btn_ins.isNull()) {
    // deallocate existing button pins
    for (const auto &button : buttons) PinManager::deallocatePin(button.pin, PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button
    buttons.clear(); // clear existing buttons
    unsigned s = 0;
    for (JsonObject btn : hw_btn_ins) {
      uint8_t type = btn["type"] | BTN_TYPE_NONE;
      int8_t  pin  = btn["pin"][0] | -1;
      if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) {
        #ifdef ARDUINO_ARCH_ESP32
        // ESP32 only: check that analog button pin is a valid ADC gpio
        if ((type == BTN_TYPE_ANALOG) || (type == BTN_TYPE_ANALOG_INVERTED)) {
          if (digitalPinToAnalogChannel(pin) < 0) {
            // not an ADC analog pin
            DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), pin, s);
            PinManager::deallocatePin(pin, PinOwner::Button);
            pin = -1;
            continue;
          } else {
            analogReadResolution(12); // see #4040
          }
        } else if ((type == BTN_TYPE_TOUCH || type == BTN_TYPE_TOUCH_SWITCH)) {
          if (digitalPinToTouchChannel(pin) < 0) {
            // not a touch pin
            DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), pin, s);
            PinManager::deallocatePin(pin, PinOwner::Button);
            pin = -1;
            continue;
          }          
          //if touch pin, enable the touch interrupt on ESP32 S2 & S3
          #ifdef SOC_TOUCH_VERSION_2    // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so
          else touchAttachInterrupt(pin, touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000)
          #endif
        } else
        #endif
        {
          // regular buttons and switches
          if (disablePullUp) {
            pinMode(pin, INPUT);
          } else {
            #ifdef ESP32
            pinMode(pin, type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
            #else
            pinMode(pin, INPUT_PULLUP);
            #endif
          }
        }
        JsonArray hw_btn_ins_0_macros = btn["macros"];
        uint8_t press       = hw_btn_ins_0_macros[0] | 0;
        uint8_t longPress   = hw_btn_ins_0_macros[1] | 0;
        uint8_t doublePress = hw_btn_ins_0_macros[2] | 0;
        buttons.emplace_back(pin, type, press, longPress, doublePress); // add button to vector
      }
      if (++s >= WLED_MAX_BUTTONS) break; // max buttons reached
    }
  } else if (fromFS) {
    // new install/missing configuration (button 0 has defaults)
    // relies upon only being called once with fromFS == true, which is currently true.
    constexpr uint8_t  defTypes[] = {BTNTYPE};
    constexpr int8_t   defPins[]  = {BTNPIN};
    constexpr unsigned numTypes   = (sizeof(defTypes) / sizeof(defTypes[0]));
    constexpr unsigned numPins    = (sizeof(defPins) / sizeof(defPins[0]));
    // check if the number of pins and types are valid; count of pins must be greater than or equal to types
    static_assert(numTypes <= numPins, "The default button pins defined in BTNPIN do not match the button types defined in BTNTYPE");

    uint8_t type = BTN_TYPE_NONE;
    buttons.clear(); // clear existing buttons (just in case)
    for (size_t s = 0; s < WLED_MAX_BUTTONS && s < numPins; s++) {
      type = defTypes[s < numTypes ? s : numTypes - 1]; // use last known type to set current type if types less than pins
      if (type == BTN_TYPE_NONE || defPins[s] < 0 || !PinManager::allocatePin(defPins[s], false, PinOwner::Button)) {
        if (buttons.size() == 0) buttons.emplace_back(-1, BTN_TYPE_NONE); // add disabled button to vector (so we have at least one button defined)
        continue; // pin not available or invalid, skip configuring this GPIO
      }
      if (disablePullUp) {
        pinMode(defPins[s], INPUT);
      } else {
        #ifdef ESP32
        pinMode(defPins[s], type==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP);
        #else
        pinMode(defPins[s], INPUT_PULLUP);
        #endif
      }
      buttons.emplace_back(defPins[s], type); // add button to vector
    }
  }

  CJSON(buttonPublishMqtt, btn_obj["mqtt"]);

  #ifndef WLED_DISABLE_INFRARED
  int hw_ir_pin = hw["ir"]["pin"] | -2; // 4
  if (hw_ir_pin > -2) {
    PinManager::deallocatePin(irPin, PinOwner::IR);
    if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) {
      irPin = hw_ir_pin;
    } else {
      irPin = -1;
    }
  }
  CJSON(irEnabled, hw["ir"]["type"]);
  #endif
  CJSON(irApplyToAllSelected, hw["ir"]["sel"]);

  JsonObject relay = hw[F("relay")];

  rlyOpenDrain  = relay[F("odrain")] | rlyOpenDrain;
  int hw_relay_pin = relay["pin"] | -2;
  if (hw_relay_pin > -2) {
    PinManager::deallocatePin(rlyPin, PinOwner::Relay);
    if (PinManager::allocatePin(hw_relay_pin,true, PinOwner::Relay)) {
      rlyPin = hw_relay_pin;
      pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT);
    } else {
      rlyPin = -1;
    }
  }
  if (relay.containsKey("rev")) {
    rlyMde = !relay["rev"];
  }

  CJSON(serialBaud, hw[F("baud")]);
  if (serialBaud < 96 || serialBaud > 15000) serialBaud = 1152;
  updateBaudRate(serialBaud *100);

  JsonArray hw_if_i2c = hw[F("if")][F("i2c-pin")];
  CJSON(i2c_sda, hw_if_i2c[0]);
  CJSON(i2c_scl, hw_if_i2c[1]);
  PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } };
  if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) {
    #ifdef ESP32
    if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior)
    else Wire.begin();
    #else
    Wire.begin(i2c_sda, i2c_scl);
    #endif
  } else {
    i2c_sda = -1;
    i2c_scl = -1;
  }
  JsonArray hw_if_spi = hw[F("if")][F("spi-pin")];
  CJSON(spi_mosi, hw_if_spi[0]);
  CJSON(spi_sclk, hw_if_spi[1]);
  CJSON(spi_miso, hw_if_spi[2]);
  PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } };
  if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) {
    #ifdef ESP32
    SPI.begin(spi_sclk, spi_miso, spi_mosi);  // SPI global uses VSPI on ESP32 and FSPI on C3, S3
    #else
    SPI.begin();
    #endif
  } else {
    spi_mosi = -1;
    spi_miso = -1;
    spi_sclk = -1;
  }

  //int hw_status_pin = hw[F("status")]["pin"]; // -1

  JsonObject light = doc[F("light")];
  CJSON(briMultiplier, light[F("scale-bri")]);
  CJSON(paletteBlend, light[F("pal-mode")]);
  CJSON(strip.autoSegments, light[F("aseg")]);

  CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.2
  float light_gc_bri = light["gc"]["bri"] | 1.0f; // default to 1.0 (false)
  float light_gc_col = light["gc"]["col"] | gammaCorrectVal; // default to gammaCorrectVal (true)
  if (light_gc_bri != 1.0f) gammaCorrectBri = true;
  else                      gammaCorrectBri = false;
  if (light_gc_col != 1.0f) gammaCorrectCol = true;
  else                      gammaCorrectCol = false;
  if (gammaCorrectVal < 0.1f || gammaCorrectVal > 3) {
    gammaCorrectVal = 1.0f; // no gamma correction
    gammaCorrectBri = false;
    gammaCorrectCol = false;
  }
  NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables

  JsonObject light_tr = light["tr"];
  int tdd = light_tr["dur"] | -1;
  if (tdd >= 0) transitionDelay = transitionDelayDefault = tdd * 100;
  strip.setTransition(transitionDelayDefault);
  CJSON(randomPaletteChangeTime, light_tr[F("rpc")]);
  CJSON(useHarmonicRandomPalette, light_tr[F("hrp")]);

  JsonObject light_nl = light["nl"];
  CJSON(nightlightMode, light_nl["mode"]);
  byte prev = nightlightDelayMinsDefault;
  CJSON(nightlightDelayMinsDefault, light_nl["dur"]);
  if (nightlightDelayMinsDefault != prev) nightlightDelayMins = nightlightDelayMinsDefault;

  CJSON(nightlightTargetBri, light_nl[F("tbri")]);
  CJSON(macroNl, light_nl["macro"]);

  JsonObject def = doc["def"];
  CJSON(bootPreset, def["ps"]);
  CJSON(turnOnAtBoot, def["on"]); // true
  CJSON(briS, def["bri"]); // 128

  JsonObject interfaces = doc["if"];

  JsonObject if_sync = interfaces["sync"];
  CJSON(udpPort, if_sync[F("port0")]); // 21324
  CJSON(udpPort2, if_sync[F("port1")]); // 65506

#ifndef WLED_DISABLE_ESPNOW
  CJSON(useESPNowSync, if_sync[F("espnow")]);
#endif

  JsonObject if_sync_recv = if_sync[F("recv")];
  CJSON(receiveNotificationBrightness, if_sync_recv["bri"]);
  CJSON(receiveNotificationColor, if_sync_recv["col"]);
  CJSON(receiveNotificationEffects, if_sync_recv["fx"]);
  CJSON(receiveNotificationPalette, if_sync_recv["pal"]);
  CJSON(receiveGroups, if_sync_recv["grp"]);
  CJSON(receiveSegmentOptions, if_sync_recv["seg"]);
  CJSON(receiveSegmentBounds, if_sync_recv["sb"]);

  JsonObject if_sync_send = if_sync[F("send")];
  CJSON(sendNotifications, if_sync_send["en"]);
  sendNotificationsRT = sendNotifications;
  CJSON(notifyDirect, if_sync_send[F("dir")]);
  CJSON(notifyButton, if_sync_send["btn"]);
  CJSON(notifyAlexa, if_sync_send["va"]);
  CJSON(notifyHue, if_sync_send["hue"]);
  CJSON(syncGroups, if_sync_send["grp"]);
  if (if_sync_send[F("twice")]) udpNumRetries = 1; // import setting from 0.13 and earlier
  CJSON(udpNumRetries, if_sync_send["ret"]);

  JsonObject if_nodes = interfaces["nodes"];
  CJSON(nodeListEnabled, if_nodes[F("list")]);
  CJSON(nodeBroadcastEnabled, if_nodes[F("bcast")]);

  JsonObject if_live = interfaces["live"];
  CJSON(receiveDirect, if_live["en"]);  // UDP/Hyperion realtime
  CJSON(useMainSegmentOnly, if_live[F("mso")]);
  CJSON(realtimeRespectLedMaps, if_live[F("rlm")]);
  CJSON(e131Port, if_live["port"]); // 5568
  if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation
  CJSON(e131Multicast, if_live[F("mc")]);

  JsonObject if_live_dmx = if_live["dmx"];
  CJSON(e131Universe, if_live_dmx[F("uni")]);
  CJSON(e131SkipOutOfSequence, if_live_dmx[F("seqskip")]);
  CJSON(DMXAddress, if_live_dmx[F("addr")]);
  if (!DMXAddress || DMXAddress > 510) DMXAddress = 1;
  CJSON(DMXSegmentSpacing, if_live_dmx[F("dss")]);
  if (DMXSegmentSpacing > 150) DMXSegmentSpacing = 0;
  CJSON(e131Priority, if_live_dmx[F("e131prio")]);
  if (e131Priority > 200) e131Priority = 200;
  CJSON(DMXMode, if_live_dmx["mode"]);

  tdd = if_live[F("timeout")] | -1;
  if (tdd >= 0) realtimeTimeoutMs = tdd * 100;

  #ifdef WLED_ENABLE_DMX_INPUT
    CJSON(dmxInputTransmitPin, if_live_dmx[F("inputRxPin")]);
    CJSON(dmxInputReceivePin, if_live_dmx[F("inputTxPin")]);
    CJSON(dmxInputEnablePin, if_live_dmx[F("inputEnablePin")]);
    CJSON(dmxInputPort, if_live_dmx[F("dmxInputPort")]);
  #endif

  CJSON(arlsForceMaxBri, if_live[F("maxbri")]);
  CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false
  CJSON(arlsOffset, if_live[F("offset")]); // 0

#ifndef WLED_DISABLE_ALEXA
  CJSON(alexaEnabled, interfaces["va"][F("alexa")]); // false
  CJSON(macroAlexaOn, interfaces["va"]["macros"][0]);
  CJSON(macroAlexaOff, interfaces["va"]["macros"][1]);
  CJSON(alexaNumPresets, interfaces["va"]["p"]);
#endif

#ifndef WLED_DISABLE_MQTT
  JsonObject if_mqtt = interfaces["mqtt"];
  CJSON(mqttEnabled, if_mqtt["en"]);
  getStringFromJson(mqttServer, if_mqtt[F("broker")], MQTT_MAX_SERVER_LEN+1);
  CJSON(mqttPort, if_mqtt["port"]); // 1883
  getStringFromJson(mqttUser, if_mqtt[F("user")], 41);
  getStringFromJson(mqttPass, if_mqtt["psk"], 65); //normally not present due to security
  getStringFromJson(mqttClientID, if_mqtt[F("cid")], 41);

  getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], MQTT_MAX_TOPIC_LEN+1); // "wled/test"
  getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], MQTT_MAX_TOPIC_LEN+1); // ""
  CJSON(retainMqttMsg, if_mqtt[F("rtn")]);
#endif

#ifndef WLED_DISABLE_HUESYNC
  JsonObject if_hue = interfaces["hue"];
  CJSON(huePollingEnabled, if_hue["en"]);
  CJSON(huePollLightId, if_hue["id"]);
  tdd = if_hue[F("iv")] | -1;
  if (tdd >= 2) huePollIntervalMs = tdd * 100;

  JsonObject if_hue_recv = if_hue["recv"];
  CJSON(hueApplyOnOff, if_hue_recv["on"]);
  CJSON(hueApplyBri, if_hue_recv["bri"]);
  CJSON(hueApplyColor, if_hue_recv["col"]);

  JsonArray if_hue_ip = if_hue["ip"];

  for (unsigned i = 0; i < 4; i++)
    CJSON(hueIP[i], if_hue_ip[i]);
#endif

  JsonObject if_ntp = interfaces[F("ntp")];
  CJSON(ntpEnabled, if_ntp["en"]);
  getStringFromJson(ntpServerName, if_ntp[F("host")], 33); // "1.wled.pool.ntp.org"
  CJSON(currentTimezone, if_ntp[F("tz")]);
  CJSON(utcOffsetSecs, if_ntp[F("offset")]);
  CJSON(useAMPM, if_ntp[F("ampm")]);
  CJSON(longitude, if_ntp[F("ln")]);
  CJSON(latitude, if_ntp[F("lt")]);

  JsonObject ol = doc[F("ol")];
  CJSON(overlayCurrent ,ol[F("clock")]); // 0
  CJSON(countdownMode, ol[F("cntdwn")]);

  CJSON(overlayMin, ol["min"]);
  CJSON(overlayMax, ol[F("max")]);
  CJSON(analogClock12pixel, ol[F("o12pix")]);
  CJSON(analogClock5MinuteMarks, ol[F("o5m")]);
  CJSON(analogClockSecondsTrail, ol[F("osec")]);
  CJSON(analogClockSolidBlack, ol[F("osb")]);

  //timed macro rules
  JsonObject tm = doc[F("timers")];
  JsonObject cntdwn = tm[F("cntdwn")];
  JsonArray cntdwn_goal = cntdwn[F("goal")];
  CJSON(countdownYear,  cntdwn_goal[0]);
  CJSON(countdownMonth, cntdwn_goal[1]);
  CJSON(countdownDay,   cntdwn_goal[2]);
  CJSON(countdownHour,  cntdwn_goal[3]);
  CJSON(countdownMin,   cntdwn_goal[4]);
  CJSON(countdownSec,   cntdwn_goal[5]);
  CJSON(macroCountdown, cntdwn["macro"]);
  setCountdown();

  JsonArray timersArray = tm["ins"];
  if (!timersArray.isNull()) {
    clearTimers();
    for (JsonObject timer : timersArray) {
      uint8_t h = timer[F("hour")] | 0;
      int8_t m = timer[F("min")] | 0;
      uint8_t p = timer[F("macro")] | 0;
      uint8_t dow = timer[F("dow")] | 127;
      uint8_t wd = (dow << 1) | ((timer[F("en")] | 0) ? 1 : 0);
      uint8_t ms = 1, me = 12, ds = 1, de = 31;
      JsonObject start = timer[F("start")];
      if (!start.isNull()) {
        ms = start[F("mon")] | 1;
        ds = start[F("day")] | 1;
      }
      JsonObject end = timer[F("end")];
      if (!end.isNull()) {
        me = end[F("mon")] | 12;
        de = end[F("day")] | 31;
      }
      addTimer(p, h, m, wd, ms, me, ds, de);
    }
  }

  JsonObject ota = doc["ota"];
  const char* pwd = ota["psk"]; //normally not present due to security

  bool pwdCorrect = !otaLock; //always allow access if ota not locked
  if (pwd != nullptr && strncmp(otaPass, pwd, 33) == 0) pwdCorrect = true;

  if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json)
    CJSON(otaLock, ota[F("lock")]);
    CJSON(wifiLock, ota[F("lock-wifi")]);
    #ifndef WLED_DISABLE_OTA
    CJSON(aOtaEnabled, ota[F("aota")]);
    #endif
    getStringFromJson(otaPass, pwd, 33); //normally not present due to security
    CJSON(otaSameSubnet, ota[F("same-subnet")]);
  }

  #ifdef WLED_ENABLE_DMX
  JsonObject dmx = doc["dmx"];
  CJSON(DMXChannels, dmx[F("chan")]);
  CJSON(DMXGap,dmx[F("gap")]);
  CJSON(DMXStart, dmx["start"]);
  CJSON(DMXStartLED,dmx[F("start-led")]);

  JsonArray dmx_fixmap = dmx[F("fixmap")];
  for (int i = 0; i < dmx_fixmap.size(); i++) {
    if (i > 14) break;
    CJSON(DMXFixtureMap[i],dmx_fixmap[i]);
  }

  CJSON(e131ProxyUniverse, dmx[F("e131proxy")]);
  #endif

  DEBUG_PRINTLN(F("Starting usermod config."));
  JsonObject usermods_settings = doc["um"];
  if (!usermods_settings.isNull()) {
    needsSave = !UsermodManager::readFromConfig(usermods_settings);
  }

  if (fromFS) return needsSave;
  // if from /json/cfg
  doReboot = doc[F("rb")] | doReboot;
  if (doInitBusses) return false; // no save needed, will do after bus init in wled.cpp loop
  return (doc["sv"] | true);
}

static const char s_cfg_json[] PROGMEM = "/cfg.json";

bool backupConfig() {
  return backupFile(s_cfg_json);
}

bool restoreConfig() {
  return restoreFile(s_cfg_json);
}

bool verifyConfig() {
  return validateJsonFile(s_cfg_json);
}

bool configBackupExists() {
  return checkBackupExists(s_cfg_json);
}

// rename config file and reboot
// if the cfg file doesn't exist, such as after a reset, do nothing
void resetConfig() {
  if (WLED_FS.exists(s_cfg_json)) {
    DEBUG_PRINTLN(F("Reset config"));
    char backupname[32];
    snprintf_P(backupname, sizeof(backupname), PSTR("/rst.%s"), &s_cfg_json[1]);
    WLED_FS.rename(s_cfg_json, backupname);
    doReboot = true;
  }
}

bool deserializeConfigFromFS() {
  (void) deserializeConfigSec(); // success/failure is ignored intentionally. We need to read cfg.json even when wsec.json has errors.

  if (!requestJSONBufferLock(JSON_LOCK_CFG_DES)) return false;

  DEBUG_PRINTLN(F("Reading settings from /cfg.json..."));

  bool success = readObjectFromFile(s_cfg_json, nullptr, pDoc);
  if (!success || (pDoc->overflowed())) pDoc->clear(); // corrupted/too-large → same as missing: seed defaults
  // NOTE: This routine deserializes *and* applies the configuration
  //       Therefore, must also initialize ethernet from this function
  JsonObject root = pDoc->as<JsonObject>();
  bool needsSave = deserializeConfig(root, true);
  releaseJSONBufferLock();

  return needsSave;
}

void serializeConfigToFS() {
  serializeConfigSec();
  backupConfig(); // backup before writing new config

  DEBUG_PRINTLN(F("Writing settings to /cfg.json..."));

  if (!requestJSONBufferLock(JSON_LOCK_CFG_SER)) return;

  JsonObject root = pDoc->to<JsonObject>();

  serializeConfig(root);

  File f = WLED_FS.open(FPSTR(s_cfg_json), "w");
  if (f) serializeJson(root, f);
  f.close();
  releaseJSONBufferLock();

  configNeedsWrite = false;
}

void serializeConfig(JsonObject root) {
  JsonArray rev = root.createNestedArray("rev");
  rev.add(1); //major settings revision
  rev.add(0); //minor settings revision

  root[F("vid")] = VERSION;

  JsonObject id = root.createNestedObject("id");
  id[F("mdns")] = cmDNS;
  id[F("name")] = serverDescription;
#ifndef WLED_DISABLE_ALEXA
  id[F("inv")] = alexaInvocationName;
#endif
  id[F("sui")] = simplifiedUI;

  JsonObject nw = root.createNestedObject("nw");
#ifndef WLED_DISABLE_ESPNOW
  nw[F("espnow")] = enableESPNow;
  JsonArray lrem = nw.createNestedArray(F("linked_remote"));
  for (size_t i = 0; i < linked_remotes.size(); i++) {
    lrem.add(linked_remotes[i].data());
  }
#endif

  JsonArray nw_ins = nw.createNestedArray("ins");
  for (size_t n = 0; n < multiWiFi.size(); n++) {
    JsonObject wifi = nw_ins.createNestedObject();
    wifi[F("ssid")] = multiWiFi[n].clientSSID;
    wifi[F("pskl")] = strlen(multiWiFi[n].clientPass);
    char bssid[13];
    fillMAC2Str(bssid, multiWiFi[n].bssid);
    wifi[F("bssid")] = bssid;
    JsonArray wifi_ip = wifi.createNestedArray("ip");
    JsonArray wifi_gw = wifi.createNestedArray("gw");
    JsonArray wifi_sn = wifi.createNestedArray("sn");
    for (size_t i = 0; i < 4; i++) {
      wifi_ip.add(multiWiFi[n].staticIP[i]);
      wifi_gw.add(multiWiFi[n].staticGW[i]);
      wifi_sn.add(multiWiFi[n].staticSN[i]);
    }
#ifdef WLED_ENABLE_WPA_ENTERPRISE
    wifi[F("enc_type")] = multiWiFi[n].encryptionType;
    if (multiWiFi[n].encryptionType == WIFI_ENCRYPTION_TYPE_ENTERPRISE) {
      wifi[F("e_anon_ident")] = multiWiFi[n].enterpriseAnonIdentity;
      wifi[F("e_ident")] = multiWiFi[n].enterpriseIdentity;
    }
#endif
  }

  JsonArray dns = nw.createNestedArray(F("dns"));
  for (size_t i = 0; i < 4; i++) {
    dns.add(dnsAddress[i]);
  }

  JsonObject ap = root.createNestedObject("ap");
  ap[F("ssid")] = apSSID;
  ap[F("pskl")] = strlen(apPass);
  ap[F("chan")] = apChannel;
  ap[F("hide")] = apHide;
  ap[F("behav")] = apBehavior;

  JsonArray ap_ip = ap.createNestedArray("ip");
  ap_ip.add(4);
  ap_ip.add(3);
  ap_ip.add(2);
  ap_ip.add(1);

  JsonObject wifi = root.createNestedObject(F("wifi"));
  wifi[F("sleep")] = !noWifiSleep;
  wifi[F("phy")] = force802_3g;
#ifdef ARDUINO_ARCH_ESP32
  wifi[F("txpwr")] = txPower;
#endif

#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET)
  JsonObject ethernet = root.createNestedObject("eth");
  ethernet["type"] = ethernetType;
  if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) {
    JsonArray pins = ethernet.createNestedArray("pin");
    for (unsigned p=0; p<WLED_ETH_RSVD_PINS_COUNT; p++) pins.add(esp32_nonconfigurable_ethernet_pins[p].pin);
    if (ethernetBoards[ethernetType].eth_power>=0)     pins.add(ethernetBoards[ethernetType].eth_power);
    if (ethernetBoards[ethernetType].eth_mdc>=0)       pins.add(ethernetBoards[ethernetType].eth_mdc);
    if (ethernetBoards[ethernetType].eth_mdio>=0)      pins.add(ethernetBoards[ethernetType].eth_mdio);
    switch (ethernetBoards[ethernetType].eth_clk_mode) {
      case ETH_CLOCK_GPIO0_IN:
      case ETH_CLOCK_GPIO0_OUT:
        pins.add(0);
        break;
      case ETH_CLOCK_GPIO16_OUT:
        pins.add(16);
        break;
      case ETH_CLOCK_GPIO17_OUT:
        pins.add(17);
        break;
    }
  }
#endif

  JsonObject hw = root.createNestedObject(F("hw"));

  JsonObject hw_led = hw.createNestedObject("led");
  hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL
  hw_led[F("maxpwr")] = BusManager::ablMilliampsMax();
//  hw_led[F("ledma")] = 0; // no longer used
  hw_led["cct"] = strip.correctWB;
  hw_led[F("cr")] = strip.cctFromRgb;
  hw_led[F("ic")] = cctICused;
  hw_led[F("cb")] = Bus::getCCTBlend();
  hw_led["fps"] = strip.getTargetFps();
  hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override

  #ifndef WLED_DISABLE_2D
  // 2D Matrix Settings
  if (strip.isMatrix) {
    JsonObject matrix = hw_led.createNestedObject(F("matrix"));
    matrix[F("mpc")] = strip.panel.size();
    JsonArray panels = matrix.createNestedArray(F("panels"));
    for (size_t i = 0; i < strip.panel.size(); i++) {
      JsonObject pnl = panels.createNestedObject();
      pnl["b"] = strip.panel[i].bottomStart;
      pnl["r"] = strip.panel[i].rightStart;
      pnl["v"] = strip.panel[i].vertical;
      pnl["s"] = strip.panel[i].serpentine;
      pnl["x"] = strip.panel[i].xOffset;
      pnl["y"] = strip.panel[i].yOffset;
      pnl["h"] = strip.panel[i].height;
      pnl["w"] = strip.panel[i].width;
    }
  }
  #endif

  JsonArray hw_led_ins = hw_led.createNestedArray("ins");

  for (size_t s = 0; s < BusManager::getNumBusses(); s++) {
    DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s);
    const Bus *bus = BusManager::getBus(s);
    if (!bus) break;  // Memory corruption, iterator invalid
    DEBUG_PRINTF_P(PSTR("  (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"),
      (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()),
      (int)(bus->getType() & 0x7F),
      (int)bus->getColorOrder(),
      (int)bus->isReversed(),
      (int)bus->skippedLeds(),
      (int)bus->getAutoWhiteMode(),
      (int)bus->getFrequency(),
      (int)bus->getLEDCurrent(), (int)bus->getMaxCurrent()
    );
    JsonObject ins = hw_led_ins.createNestedObject();
    ins["start"] = bus->getStart();
    ins["len"]   = bus->getLength();
    JsonArray ins_pin = ins.createNestedArray("pin");
    uint8_t pins[5];
    uint8_t nPins = bus->getPins(pins);
    for (int i = 0; i < nPins; i++) ins_pin.add(pins[i]);
    ins[F("order")]  = bus->getColorOrder();
    ins["rev"]       = bus->isReversed();
    ins[F("skip")]   = bus->skippedLeds();
    ins["type"]      = bus->getType() & 0x7F;
    ins["ref"]       = bus->isOffRefreshRequired();
    ins[F("rgbwm")]  = bus->getAutoWhiteMode();
    ins[F("freq")]   = bus->getFrequency();
    ins[F("maxpwr")] = bus->getMaxCurrent();
    ins[F("ledma")]  = bus->getLEDCurrent();
    ins[F("drv")]    = bus->getDriverType();
    ins[F("text")]   = bus->getCustomText();
  }

  JsonArray hw_com = hw.createNestedArray(F("com"));
  const ColorOrderMap& com = BusManager::getColorOrderMap();
  for (size_t s = 0; s < com.count(); s++) {
    const ColorOrderMapEntry *entry = com.get(s);
    if (!entry || !entry->len) break;
    JsonObject co = hw_com.createNestedObject();
    co["start"] = entry->start;
    co["len"] = entry->len;
    co[F("order")] = entry->colorOrder;
  }

  // button(s)
  JsonObject hw_btn = hw.createNestedObject("btn");
  hw_btn["max"] = WLED_MAX_BUTTONS; // just information about max number of buttons (not actually used)
  hw_btn[F("pull")] = !disablePullUp;
  JsonArray hw_btn_ins = hw_btn.createNestedArray("ins");

  // configuration for all buttons
  for (const auto &button : buttons) {
    JsonObject hw_btn_ins_0 = hw_btn_ins.createNestedObject();
    hw_btn_ins_0["type"] = button.type;
    JsonArray hw_btn_ins_0_pin = hw_btn_ins_0.createNestedArray("pin");
    hw_btn_ins_0_pin.add(button.pin);
    JsonArray hw_btn_ins_0_macros = hw_btn_ins_0.createNestedArray("macros");
    hw_btn_ins_0_macros.add(button.macroButton);
    hw_btn_ins_0_macros.add(button.macroLongPress);
    hw_btn_ins_0_macros.add(button.macroDoublePress);
  }

  hw_btn[F("tt")] = touchThreshold;
  hw_btn["mqtt"] = buttonPublishMqtt;

  JsonObject hw_ir = hw.createNestedObject("ir");
  #ifndef WLED_DISABLE_INFRARED
  hw_ir["pin"] = irPin;
  hw_ir["type"] = irEnabled;  // the byte 'irEnabled' does contain the IR-Remote Type ( 0=disabled )
  #endif
  hw_ir["sel"] = irApplyToAllSelected;

  JsonObject hw_relay = hw.createNestedObject(F("relay"));
  hw_relay["pin"] = rlyPin;
  hw_relay["rev"] = !rlyMde;
  hw_relay[F("odrain")] = rlyOpenDrain;

  hw[F("baud")] = serialBaud;

  JsonObject hw_if = hw.createNestedObject(F("if"));
  JsonArray hw_if_i2c = hw_if.createNestedArray("i2c-pin");
  hw_if_i2c.add(i2c_sda);
  hw_if_i2c.add(i2c_scl);
  JsonArray hw_if_spi = hw_if.createNestedArray("spi-pin");
  hw_if_spi.add(spi_mosi);
  hw_if_spi.add(spi_sclk);
  hw_if_spi.add(spi_miso);

  //JsonObject hw_status = hw.createNestedObject("status");
  //hw_status["pin"] = -1;

  JsonObject light = root.createNestedObject(F("light"));
  light[F("scale-bri")] = briMultiplier;
  light[F("pal-mode")] = paletteBlend;
  light[F("aseg")] = strip.autoSegments;

  JsonObject light_gc = light.createNestedObject("gc");
  light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f;  // keep compatibility
  light_gc["col"] = (gammaCorrectCol) ? gammaCorrectVal : 1.0f;  // keep compatibility
  light_gc["val"] = gammaCorrectVal;

  JsonObject light_tr = light.createNestedObject("tr");
  light_tr["dur"] = transitionDelayDefault / 100;
  light_tr[F("rpc")] = randomPaletteChangeTime;
  light_tr[F("hrp")] = useHarmonicRandomPalette;

  JsonObject light_nl = light.createNestedObject("nl");
  light_nl["mode"] = nightlightMode;
  light_nl["dur"] = nightlightDelayMinsDefault;
  light_nl[F("tbri")] = nightlightTargetBri;
  light_nl["macro"] = macroNl;

  JsonObject def = root.createNestedObject("def");
  def["ps"] = bootPreset;
  def["on"] = turnOnAtBoot;
  def["bri"] = briS;

  JsonObject interfaces = root.createNestedObject("if");

  JsonObject if_sync = interfaces.createNestedObject("sync");
  if_sync[F("port0")] = udpPort;
  if_sync[F("port1")] = udpPort2;

#ifndef WLED_DISABLE_ESPNOW
  if_sync[F("espnow")] = useESPNowSync;
#endif

  JsonObject if_sync_recv = if_sync.createNestedObject(F("recv"));
  if_sync_recv["bri"] = receiveNotificationBrightness;
  if_sync_recv["col"] = receiveNotificationColor;
  if_sync_recv["fx"]  = receiveNotificationEffects;
  if_sync_recv["pal"] = receiveNotificationPalette;
  if_sync_recv["grp"] = receiveGroups;
  if_sync_recv["seg"] = receiveSegmentOptions;
  if_sync_recv["sb"]  = receiveSegmentBounds;

  JsonObject if_sync_send = if_sync.createNestedObject(F("send"));
  if_sync_send["en"] = sendNotifications;
  if_sync_send[F("dir")] = notifyDirect;
  if_sync_send["btn"] = notifyButton;
  if_sync_send["va"] = notifyAlexa;
  if_sync_send["hue"] = notifyHue;
  if_sync_send["grp"] = syncGroups;
  if_sync_send["ret"] = udpNumRetries;

  JsonObject if_nodes = interfaces.createNestedObject("nodes");
  if_nodes[F("list")] = nodeListEnabled;
  if_nodes[F("bcast")] = nodeBroadcastEnabled;

  JsonObject if_live = interfaces.createNestedObject("live");
  if_live["en"] = receiveDirect; // UDP/Hyperion realtime
  if_live[F("mso")] = useMainSegmentOnly;
  if_live[F("rlm")] = realtimeRespectLedMaps;
  if_live["port"] = e131Port;
  if_live[F("mc")] = e131Multicast;

  JsonObject if_live_dmx = if_live.createNestedObject("dmx");
  if_live_dmx[F("uni")] = e131Universe;
  if_live_dmx[F("seqskip")] = e131SkipOutOfSequence;
  if_live_dmx[F("e131prio")] = e131Priority;
  if_live_dmx[F("addr")] = DMXAddress;
  if_live_dmx[F("dss")] = DMXSegmentSpacing;
  if_live_dmx["mode"] = DMXMode;
  #ifdef WLED_ENABLE_DMX_INPUT
    if_live_dmx[F("inputRxPin")] = dmxInputTransmitPin;
    if_live_dmx[F("inputTxPin")] = dmxInputReceivePin;
    if_live_dmx[F("inputEnablePin")] = dmxInputEnablePin;
    if_live_dmx[F("dmxInputPort")] = dmxInputPort;
  #endif

  if_live[F("timeout")] = realtimeTimeoutMs / 100;
  if_live[F("maxbri")] = arlsForceMaxBri;
  if_live[F("no-gc")] = arlsDisableGammaCorrection;
  if_live[F("offset")] = arlsOffset;

#ifndef WLED_DISABLE_ALEXA
  JsonObject if_va = interfaces.createNestedObject("va");
  if_va[F("alexa")] = alexaEnabled;

  JsonArray if_va_macros = if_va.createNestedArray("macros");
  if_va_macros.add(macroAlexaOn);
  if_va_macros.add(macroAlexaOff);

  if_va["p"] = alexaNumPresets;
#endif

#ifndef WLED_DISABLE_MQTT
  JsonObject if_mqtt = interfaces.createNestedObject("mqtt");
  if_mqtt["en"] = mqttEnabled;
  if_mqtt[F("broker")] = mqttServer;
  if_mqtt["port"] = mqttPort;
  if_mqtt[F("user")] = mqttUser;
  if_mqtt[F("pskl")] = strlen(mqttPass);
  if_mqtt[F("cid")] = mqttClientID;
  if_mqtt[F("rtn")] = retainMqttMsg;

  JsonObject if_mqtt_topics = if_mqtt.createNestedObject(F("topics"));
  if_mqtt_topics[F("device")] = mqttDeviceTopic;
  if_mqtt_topics[F("group")] = mqttGroupTopic;
#endif

#ifndef WLED_DISABLE_HUESYNC
  JsonObject if_hue = interfaces.createNestedObject("hue");
  if_hue["en"] = huePollingEnabled;
  if_hue["id"] = huePollLightId;
  if_hue[F("iv")] = huePollIntervalMs / 100;

  JsonObject if_hue_recv = if_hue.createNestedObject(F("recv"));
  if_hue_recv["on"] = hueApplyOnOff;
  if_hue_recv["bri"] = hueApplyBri;
  if_hue_recv["col"] = hueApplyColor;

  JsonArray if_hue_ip = if_hue.createNestedArray("ip");
  for (unsigned i = 0; i < 4; i++) {
    if_hue_ip.add(hueIP[i]);
  }
#endif

  JsonObject if_ntp = interfaces.createNestedObject("ntp");
  if_ntp["en"] = ntpEnabled;
  if_ntp[F("host")] = ntpServerName;
  if_ntp[F("tz")] = currentTimezone;
  if_ntp[F("offset")] = utcOffsetSecs;
  if_ntp[F("ampm")] = useAMPM;
  if_ntp[F("ln")] = longitude;
  if_ntp[F("lt")] = latitude;

  JsonObject ol = root.createNestedObject("ol");
  ol[F("clock")] = overlayCurrent;
  ol[F("cntdwn")] = countdownMode;

  ol["min"] = overlayMin;
  ol[F("max")] = overlayMax;
  ol[F("o12pix")] = analogClock12pixel;
  ol[F("o5m")] = analogClock5MinuteMarks;
  ol[F("osec")] = analogClockSecondsTrail;
  ol[F("osb")] = analogClockSolidBlack;

  JsonObject timers = root.createNestedObject(F("timers"));

  JsonObject cntdwn = timers.createNestedObject(F("cntdwn"));
  JsonArray goal = cntdwn.createNestedArray(F("goal"));
  goal.add(countdownYear); goal.add(countdownMonth); goal.add(countdownDay);
  goal.add(countdownHour); goal.add(countdownMin); goal.add(countdownSec);
  cntdwn["macro"] = macroCountdown;

  JsonArray timers_ins = timers.createNestedArray("ins");
  for (size_t i = 0; i < ::timers.size(); i++) {
    const Timer& t = ::timers[i];
    if (t.preset == 0 && t.hour == 0 && t.minute == 0) continue;
    JsonObject ti = timers_ins.createNestedObject();
    ti[F("en")] = t.isEnabled() ? 1 : 0;
    ti[F("hour")] = t.hour;
    ti[F("min")] = t.minute;
    ti[F("macro")] = t.preset;
    ti[F("dow")] = t.weekdays >> 1;
    JsonObject start = ti.createNestedObject(F("start"));
    start[F("mon")] = t.monthStart;
    start[F("day")] = t.dayStart;
    JsonObject end = ti.createNestedObject(F("end"));
    end[F("mon")] = t.monthEnd;
    end[F("day")] = t.dayEnd;
  }

  JsonObject ota = root.createNestedObject("ota");
  ota[F("lock")] = otaLock;
  ota[F("lock-wifi")] = wifiLock;
  ota[F("pskl")] = strlen(otaPass);
  #ifndef WLED_DISABLE_OTA
  ota[F("aota")] = aOtaEnabled;
  #endif
  ota[F("same-subnet")] = otaSameSubnet;

  #ifdef WLED_ENABLE_DMX
  JsonObject dmx = root.createNestedObject("dmx");
  dmx[F("chan")] = DMXChannels;
  dmx[F("gap")] = DMXGap;
  dmx["start"] = DMXStart;
  dmx[F("start-led")] = DMXStartLED;

  JsonArray dmx_fixmap = dmx.createNestedArray(F("fixmap"));
  for (unsigned i = 0; i < 15; i++) {
    dmx_fixmap.add(DMXFixtureMap[i]);
  }

  dmx[F("e131proxy")] = e131ProxyUniverse;
  #endif

  JsonObject usermods_settings = root.createNestedObject("um");
  UsermodManager::addToConfig(usermods_settings);
}


static const char s_wsec_json[] PROGMEM = "/wsec.json";

//settings in /wsec.json, not accessible via webserver, for passwords and tokens
bool deserializeConfigSec() {
  DEBUG_PRINTLN(F("Reading settings from /wsec.json..."));

  if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_DES)) return false;

  bool success = readObjectFromFile(s_wsec_json, nullptr, pDoc);
  if (!success || pDoc->overflowed() || pDoc->size() < 1) { // reject if overflowed (noMemory) or empty
    releaseJSONBufferLock();
    return false;
  }

  JsonObject root = pDoc->as<JsonObject>();

  size_t n = 0;
  JsonArray nw_ins = root["nw"]["ins"];
  if (!nw_ins.isNull()) {
    size_t newSize = min((size_t)WLED_MAX_WIFI_COUNT, nw_ins.size());               // cap at WLED_MAX_WIFI_COUNT (prevent oversizing when too many Wi-Fi entries)
    if (nw_ins.size() > 1 && newSize > multiWiFi.size()) multiWiFi.resize(newSize); // resize constructs objects while resizing
    for (JsonObject wifi : nw_ins) {
      char pw[65] = "";
      getStringFromJson(pw, wifi["psk"], 65);
      strlcpy(multiWiFi[n].clientPass, pw, 65);
      if (++n >= WLED_MAX_WIFI_COUNT) break;
    }
  }

  JsonObject ap = root["ap"];
  getStringFromJson(apPass, ap["psk"] , 65);

  [[maybe_unused]] JsonObject interfaces = root["if"];

#ifndef WLED_DISABLE_MQTT
  JsonObject if_mqtt = interfaces["mqtt"];
  getStringFromJson(mqttPass, if_mqtt["psk"], 65);
#endif

#ifndef WLED_DISABLE_HUESYNC
  getStringFromJson(hueApiKey, interfaces["hue"][F("key")], 47);
#endif

  getStringFromJson(settingsPIN, root["pin"], 5);
  correctPIN = !strlen(settingsPIN);

  JsonObject ota = root["ota"];
  getStringFromJson(otaPass, ota[F("pwd")], 33);
  CJSON(otaLock, ota[F("lock")]);
  CJSON(wifiLock, ota[F("lock-wifi")]);
  #ifndef WLED_DISABLE_OTA
  CJSON(aOtaEnabled, ota[F("aota")]);
  #endif

  releaseJSONBufferLock();
  return true;
}

void serializeConfigSec() {
  DEBUG_PRINTLN(F("Writing settings to /wsec.json..."));

  if (!requestJSONBufferLock(JSON_LOCK_CFG_SEC_SER)) return;

  JsonObject root = pDoc->to<JsonObject>();

  JsonObject nw = root.createNestedObject("nw");

  JsonArray nw_ins = nw.createNestedArray("ins");
  for (size_t i = 0; i < multiWiFi.size(); i++) {
    JsonObject wifi = nw_ins.createNestedObject();
    wifi[F("psk")] = multiWiFi[i].clientPass;
  }

  JsonObject ap = root.createNestedObject("ap");
  ap["psk"] = apPass;

  [[maybe_unused]] JsonObject interfaces = root.createNestedObject("if");
#ifndef WLED_DISABLE_MQTT
  JsonObject if_mqtt = interfaces.createNestedObject("mqtt");
  if_mqtt["psk"] = mqttPass;
#endif
#ifndef WLED_DISABLE_HUESYNC
  JsonObject if_hue = interfaces.createNestedObject("hue");
  if_hue[F("key")] = hueApiKey;
#endif

  root["pin"] = settingsPIN;

  JsonObject ota = root.createNestedObject("ota");
  ota[F("pwd")] = otaPass;
  ota[F("lock")] = otaLock;
  ota[F("lock-wifi")] = wifiLock;
  #ifndef WLED_DISABLE_OTA
  ota[F("aota")] = aOtaEnabled;
  #endif

  File f = WLED_FS.open(FPSTR(s_wsec_json), "w");
  if (f) serializeJson(root, f);
  f.close();
  releaseJSONBufferLock();
}
