/*
  support.ino - support for Tasmota

  Copyright (C) 2020  Theo Arends

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

IPAddress syslog_host_addr;      // Syslog host IP address
uint32_t syslog_host_hash = 0;   // Syslog host name hash

extern "C" {
extern struct rst_info resetInfo;
}

/*********************************************************************************************\
 * Watchdog extension (https://github.com/esp8266/Arduino/issues/1532)
\*********************************************************************************************/

#include <Ticker.h>

Ticker tickerOSWatch;

const uint32_t OSWATCH_RESET_TIME = 120;

static unsigned long oswatch_last_loop_time;
uint8_t oswatch_blocked_loop = 0;

#ifndef USE_WS2812_DMA  // Collides with Neopixelbus but solves exception
//void OsWatchTicker() ICACHE_RAM_ATTR;
#endif  // USE_WS2812_DMA

#ifdef USE_KNX
bool knx_started = false;
#endif  // USE_KNX

void OsWatchTicker(void)
{
  uint32_t t = millis();
  uint32_t last_run = t - oswatch_last_loop_time;

#ifdef DEBUG_THEO
  int32_t rssi = WiFi.RSSI();
  AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_APPLICATION D_OSWATCH " FreeRam %d, rssi %d %% (%d dBm), last_run %d"), ESP_getFreeHeap(), WifiGetRssiAsQuality(rssi), rssi, last_run);
#endif  // DEBUG_THEO
  if (last_run >= (OSWATCH_RESET_TIME * 1000)) {
//    AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_OSWATCH " " D_BLOCKED_LOOP ". " D_RESTARTING));  // Save iram space
    RtcSettings.oswatch_blocked_loop = 1;
    RtcSettingsSave();

//    ESP.restart();  // normal reboot
//    ESP.reset();  // hard reset

    // Force an exception to get a stackdump
    volatile uint32_t dummy;
    dummy = *((uint32_t*) 0x00000000);
    (void)dummy;    // avoid compiler warning
  }
}

void OsWatchInit(void)
{
  oswatch_blocked_loop = RtcSettings.oswatch_blocked_loop;
  RtcSettings.oswatch_blocked_loop = 0;
  oswatch_last_loop_time = millis();
  tickerOSWatch.attach_ms(((OSWATCH_RESET_TIME / 3) * 1000), OsWatchTicker);
}

void OsWatchLoop(void)
{
  oswatch_last_loop_time = millis();
//  while(1) delay(1000);  // this will trigger the os watch
}

bool OsWatchBlockedLoop(void)
{
  return oswatch_blocked_loop;
}

uint32_t ResetReason(void)
{
  /*
    user_interface.h
    REASON_DEFAULT_RST      = 0,  // "Power on"                normal startup by power on
    REASON_WDT_RST          = 1,  // "Hardware Watchdog"       hardware watch dog reset
    REASON_EXCEPTION_RST    = 2,  // "Exception"               exception reset, GPIO status won’t change
    REASON_SOFT_WDT_RST     = 3,  // "Software Watchdog"       software watch dog reset, GPIO status won’t change
    REASON_SOFT_RESTART     = 4,  // "Software/System restart" software restart ,system_restart , GPIO status won’t change
    REASON_DEEP_SLEEP_AWAKE = 5,  // "Deep-Sleep Wake"         wake up from deep-sleep
    REASON_EXT_SYS_RST      = 6   // "External System"         external system reset
  */
  return ESP_ResetInfoReason();
}

String GetResetReason(void)
{
  if (oswatch_blocked_loop) {
    char buff[32];
    strncpy_P(buff, PSTR(D_JSON_BLOCKED_LOOP), sizeof(buff));
    return String(buff);
  } else {
    return ESP_getResetReason();
  }
}

/*********************************************************************************************\
 * Miscellaneous
\*********************************************************************************************/
/*
String GetBinary(const void* ptr, size_t count) {
  uint32_t value = *(uint32_t*)ptr;
  value <<= (32 - count);
  String result;
  result.reserve(count + 1);
  for (uint32_t i = 0; i < count; i++) {
    result += (value &0x80000000) ? '1' : '0';
    value <<= 1;
  }
  return result;
}
*/
String GetBinary8(uint8_t value, size_t count) {
  if (count > 8) { count = 8; }
  value <<= (8 - count);
  String result;
  result.reserve(count + 1);
  for (uint32_t i = 0; i < count; i++) {
    result += (value &0x80) ? '1' : '0';
    value <<= 1;
  }
  return result;
}

// Get span until single character in string
size_t strchrspn(const char *str1, int character)
{
  size_t ret = 0;
  char *start = (char*)str1;
  char *end = strchr(str1, character);
  if (end) ret = end - start;
  return ret;
}

uint32_t ChrCount(const char *str, const char *delim) {
  uint32_t count = 0;
  char* read = (char*)str;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    if (ch == *delim) { count++; }
  }
  return count;
}

// Function to return a substring defined by a delimiter at an index
char* subStr(char* dest, char* str, const char *delim, int index)
{
  char *act;
  char *sub = nullptr;
  char *ptr;
  int i;

  // Since strtok consumes the first arg, make a copy
  strncpy(dest, str, strlen(str)+1);
  for (i = 1, act = dest; i <= index; i++, act = nullptr) {
    sub = strtok_r(act, delim, &ptr);
    if (sub == nullptr) break;
  }
  sub = Trim(sub);
  return sub;
}

float CharToFloat(const char *str)
{
  // simple ascii to double, because atof or strtod are too large
  char strbuf[24];

  strlcpy(strbuf, str, sizeof(strbuf));
  char *pt = strbuf;
  if (*pt == '\0') { return 0.0; }

  while ((*pt != '\0') && isblank(*pt)) { pt++; }  // Trim leading spaces

  signed char sign = 1;
  if (*pt == '-') { sign = -1; }
  if (*pt == '-' || *pt == '+') { pt++; }          // Skip any sign

  float left = 0;
  if (*pt != '.') {
    left = atoi(pt);                               // Get left part
    while (isdigit(*pt)) { pt++; }                 // Skip number
  }

  float right = 0;
  if (*pt == '.') {
    pt++;
    uint32_t max_decimals = 0;
    while ((max_decimals < 8) && isdigit(pt[max_decimals])) { max_decimals++; }
    pt[max_decimals] = '\0';                       // Limit decimals to float max of 8
    right = atoi(pt);                              // Decimal part
    while (isdigit(*pt)) {
      pt++;
      right /= 10.0f;
    }
  }

  float result = left + right;
  if (sign < 0) {
    return -result;                                // Add negative sign
  }
  return result;
}

int TextToInt(char *str)
{
  char *p;
  uint8_t radix = 10;
  if ('#' == str[0]) {
    radix = 16;
    str++;
  }
  return strtol(str, &p, radix);
}

char* ulltoa(unsigned long long value, char *str, int radix)
{
  char digits[64];
  char *dst = str;
  int i = 0;

//  if (radix < 2 || radix > 36) { radix = 10; }

  do {
    int n = value % radix;
    digits[i++] = (n < 10) ? (char)n+'0' : (char)n-10+'A';
    value /= radix;
  } while (value != 0);

  while (i > 0) { *dst++ = digits[--i]; }

  *dst = 0;
  return str;
}

// see https://stackoverflow.com/questions/6357031/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-in-c
// char* ToHex_P(unsigned char * in, size_t insz, char * out, size_t outsz, char inbetween = '\0'); in tasmota_globals.h
char* ToHex_P(const unsigned char * in, size_t insz, char * out, size_t outsz, char inbetween)
{
  // ToHex_P(in, insz, out, outz)      -> "12345667"
  // ToHex_P(in, insz, out, outz, ' ') -> "12 34 56 67"
  // ToHex_P(in, insz, out, outz, ':') -> "12:34:56:67"
  static const char * hex = "0123456789ABCDEF";
  int between = (inbetween) ? 3 : 2;
  const unsigned char * pin = in;
  char * pout = out;
  for (; pin < in+insz; pout += between, pin++) {
    pout[0] = hex[(pgm_read_byte(pin)>>4) & 0xF];
    pout[1] = hex[ pgm_read_byte(pin)     & 0xF];
    if (inbetween) { pout[2] = inbetween; }
    if (pout + 3 - out > outsz) { break; }  // Better to truncate output string than overflow buffer
  }
  pout[(inbetween && insz) ? -1 : 0] = 0;   // Discard last inbetween if any input
  return out;
}

char* Uint64toHex(uint64_t value, char *str, uint16_t bits)
{
  ulltoa(value, str, 16);  // Get 64bit value

  int fill = 8;
  if ((bits > 3) && (bits < 65)) {
    fill = bits / 4;  // Max 16
    if (bits % 4) { fill++; }
  }
  int len = strlen(str);
  fill -= len;
  if (fill > 0) {
    memmove(str + fill, str, len +1);
    memset(str, '0', fill);
  }
  return str;
}

char* dtostrfd(double number, unsigned char prec, char *s)
{
  if ((isnan(number)) || (isinf(number))) {  // Fix for JSON output (https://stackoverflow.com/questions/1423081/json-left-out-infinity-and-nan-json-status-in-ecmascript)
    strcpy(s, "null");
    return s;
  } else {
    return dtostrf(number, 1, prec, s);
  }
}

char* Unescape(char* buffer, uint32_t* size)
{
  uint8_t* read = (uint8_t*)buffer;
  uint8_t* write = (uint8_t*)buffer;
  int32_t start_size = *size;
  int32_t end_size = *size;
  uint8_t che = 0;

//  AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)buffer, *size);

  while (start_size > 0) {
    uint8_t ch = *read++;
    start_size--;
    if (ch != '\\') {
      *write++ = ch;
    } else {
      if (start_size > 0) {
        uint8_t chi = *read++;
        start_size--;
        end_size--;
        switch (chi) {
          case '\\': che = '\\'; break;  // 5C Backslash
          case 'a': che = '\a'; break;   // 07 Bell (Alert)
          case 'b': che = '\b'; break;   // 08 Backspace
          case 'e': che = '\e'; break;   // 1B Escape
          case 'f': che = '\f'; break;   // 0C Formfeed
          case 'n': che = '\n'; break;   // 0A Linefeed (Newline)
          case 'r': che = '\r'; break;   // 0D Carriage return
          case 's': che = ' ';  break;   // 20 Space
          case 't': che = '\t'; break;   // 09 Horizontal tab
          case 'v': che = '\v'; break;   // 0B Vertical tab
          case 'x': {
            uint8_t* start = read;
            che = (uint8_t)strtol((const char*)read, (char**)&read, 16);
            start_size -= (uint16_t)(read - start);
            end_size -= (uint16_t)(read - start);
            break;
          }
          case '"': che = '\"'; break;   // 22 Quotation mark
//          case '?': che = '\?'; break;   // 3F Question mark
          default : {
            che = chi;
            *write++ = ch;
            end_size++;
          }
        }
        *write++ = che;
      }
    }
  }
  *size = end_size;
  *write++ = 0;   // add the end string pointer reference
//  AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)buffer, *size);

  return buffer;
}

char* RemoveSpace(char* p) {
  // Remove white-space character (' ','\t','\n','\v','\f','\r')
  char* write = p;
  char* read = p;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    if (!isspace(ch)) {
      *write++ = ch;
    }
  }
  return p;
}

char* RemoveControlCharacter(char* p) {
  // Remove control character (0x00 .. 0x1F and 0x7F)
  char* write = p;
  char* read = p;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    if (!iscntrl(ch)) {
      *write++ = ch;
    }
  }
  *write++ = '\0';
  return p;
}

char* ReplaceCommaWithDot(char* p) {
  // Replace character ',' with '.'
  char* write = (char*)p;
  char* read = (char*)p;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    if (ch == ',') {
      ch = '.';
    }
    *write++ = ch;
  }
  return p;
}

char* LowerCase(char* dest, const char* source)
{
  char* write = dest;
  const char* read = source;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    *write++ = tolower(ch);
  }
  return dest;
}

char* UpperCase(char* dest, const char* source)
{
  char* write = dest;
  const char* read = source;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    *write++ = toupper(ch);
  }
  return dest;
}

char* UpperCase_P(char* dest, const char* source)
{
  char* write = dest;
  const char* read = source;
  char ch = '.';

  while (ch != '\0') {
    ch = pgm_read_byte(read++);
    *write++ = toupper(ch);
  }
  return dest;
}

char* Trim(char* p)
{
  if (*p != '\0') {
    while ((*p != '\0') && isblank(*p)) { p++; }  // Trim leading spaces
    char* q = p + strlen(p) -1;
    while ((q >= p) && isblank(*q)) { q--; }   // Trim trailing spaces
    q++;
    *q = '\0';
  }
  return p;
}

/*
char* RemoveAllSpaces(char* p)
{
  // remove any white space from the base64
  char *cursor = p;
  uint32_t offset = 0;
  while (1) {
    *cursor = *(cursor + offset);
    if ((' ' == *cursor) || ('\t' == *cursor) || ('\n' == *cursor)) {   // if space found, remove this char until end of string
      offset++;
    } else {
      if (0 == *cursor) { break; }
      cursor++;
    }
  }
  return p;
}
*/

char* NoAlNumToUnderscore(char* dest, const char* source)
{
  char* write = dest;
  const char* read = source;
  char ch = '.';

  while (ch != '\0') {
    ch = *read++;
    *write++ = (isalnum(ch) || ('\0' == ch)) ? ch : '_';
  }
  return dest;
}

char IndexSeparator(void)
{
/*
  // 20 bytes more costly !?!
  const char separators[] = { "-_" };

  return separators[Settings.flag3.use_underscore];
*/
  if (Settings.flag3.use_underscore) {  // SetOption64 - Enable "_" instead of "-" as sensor index separator
    return '_';
  } else {
    return '-';
  }
}

void SetShortcutDefault(void)
{
  if ('\0' != XdrvMailbox.data[0]) {     // There must be at least one character in the buffer
    XdrvMailbox.data[0] = '0' + SC_DEFAULT;  // SC_CLEAR, SC_DEFAULT, SC_USER
    XdrvMailbox.data[1] = '\0';
  }
}

uint8_t Shortcut(void)
{
  uint8_t result = 10;

  if ('\0' == XdrvMailbox.data[1]) {    // Only allow single character input for shortcut
    if (('"' == XdrvMailbox.data[0]) || ('0' == XdrvMailbox.data[0])) {
      result = SC_CLEAR;
    } else {
      result = atoi(XdrvMailbox.data);  // 1 = SC_DEFAULT, 2 = SC_USER
      if (0 == result) {
        result = 10;
      }
    }
  }
  return result;
}

bool ValidIpAddress(const char* str)
{
  const char* p = str;

  while (*p && ((*p == '.') || ((*p >= '0') && (*p <= '9')))) { p++; }
  return (*p == '\0');
}

bool ParseIp(uint32_t* addr, const char* str)
{
  uint8_t *part = (uint8_t*)addr;
  uint8_t i;

  *addr = 0;
  for (i = 0; i < 4; i++) {
    part[i] = strtoul(str, nullptr, 10);        // Convert byte
    str = strchr(str, '.');
    if (str == nullptr || *str == '\0') {
      break;  // No more separators, exit
    }
    str++;                                   // Point to next character after separator
  }
  return (3 == i);
}

uint32_t ParseParameters(uint32_t count, uint32_t *params)
{
  char *p;
  uint32_t i = 0;
  for (char *str = strtok_r(XdrvMailbox.data, ", ", &p); str && i < count; str = strtok_r(nullptr, ", ", &p), i++) {
    params[i] = strtoul(str, nullptr, 0);
  }
  return i;
}

// Function to parse & check if version_str is newer than our currently installed version.
bool NewerVersion(char* version_str)
{
  uint32_t version = 0;
  uint32_t i = 0;
  char *str_ptr;

  char version_dup[strlen(version_str) +1];
  strncpy(version_dup, version_str, sizeof(version_dup));  // Duplicate the version_str as strtok_r will modify it.
  // Loop through the version string, splitting on '.' seperators.
  for (char *str = strtok_r(version_dup, ".", &str_ptr); str && i < sizeof(VERSION); str = strtok_r(nullptr, ".", &str_ptr), i++) {
    int field = atoi(str);
    // The fields in a version string can only range from 0-255.
    if ((field < 0) || (field > 255)) {
      return false;
    }
    // Shuffle the accumulated bytes across, and add the new byte.
    version = (version << 8) + field;
    // Check alpha delimiter after 1.2.3 only
    if ((2 == i) && isalpha(str[strlen(str)-1])) {
      field = str[strlen(str)-1] & 0x1f;
      version = (version << 8) + field;
      i++;
    }
  }
  // A version string should have 2-4 fields. e.g. 1.2, 1.2.3, or 1.2.3a (= 1.2.3.1).
  // If not, then don't consider it a valid version string.
  if ((i < 2) || (i > sizeof(VERSION))) {
    return false;
  }
  // Keep shifting the parsed version until we hit the maximum number of tokens.
  // VERSION stores the major number of the version in the most significant byte of the uint32_t.
  while (i < sizeof(VERSION)) {
    version <<= 8;
    i++;
  }
  // Now we should have a fully constructed version number in uint32_t form.
  return (version > VERSION);
}

char* GetPowerDevice(char* dest, uint32_t idx, size_t size, uint32_t option)
{
  strncpy_P(dest, S_RSLT_POWER, size);                // POWER
  if ((TasmotaGlobal.devices_present + option) > 1) {
    char sidx[8];
    snprintf_P(sidx, sizeof(sidx), PSTR("%d"), idx);  // x
    strncat(dest, sidx, size - strlen(dest) -1);      // POWERx
  }
  return dest;
}

char* GetPowerDevice(char* dest, uint32_t idx, size_t size)
{
  return GetPowerDevice(dest, idx, size, 0);
}

void GetEspHardwareType(void)
{
#ifdef ESP8266
  // esptool.py get_efuses
  uint32_t efuse1 = *(uint32_t*)(0x3FF00050);
  uint32_t efuse2 = *(uint32_t*)(0x3FF00054);
//  uint32_t efuse3 = *(uint32_t*)(0x3FF00058);
//  uint32_t efuse4 = *(uint32_t*)(0x3FF0005C);

  TasmotaGlobal.is_8285 = ( (efuse1 & (1 << 4)) || (efuse2 & (1 << 16)) );
  if (TasmotaGlobal.is_8285 && (ESP.getFlashChipRealSize() > 1048576)) {
    TasmotaGlobal.is_8285 = false;  // ESP8285 can only have 1M flash
  }
#else
  TasmotaGlobal.is_8285 = false;    // ESP8285 can only have 1M flash
#endif
}

String GetDeviceHardware(void)
{
  char buff[10];
#ifdef ESP8266
  if (TasmotaGlobal.is_8285) {
    strcpy_P(buff, PSTR("ESP8285"));
  } else {
    strcpy_P(buff, PSTR("ESP8266EX"));
  }
#endif  // ESP8266
#ifdef ESP32
  strcpy_P(buff, PSTR("ESP32"));
#endif  // ESP32
  return String(buff);
}

float ConvertTemp(float c)
{
  float result = c;

  TasmotaGlobal.global_update = TasmotaGlobal.uptime;
  TasmotaGlobal.temperature_celsius = c;

  if (!isnan(c) && Settings.flag.temperature_conversion) {    // SetOption8 - Switch between Celsius or Fahrenheit
    result = c * 1.8 + 32;                                    // Fahrenheit
  }
  result = result + (0.1 * Settings.temp_comp);
  return result;
}

float ConvertTempToCelsius(float c)
{
  float result = c;

  if (!isnan(c) && Settings.flag.temperature_conversion) {    // SetOption8 - Switch between Celsius or Fahrenheit
    result = (c - 32) / 1.8;                                  // Celsius
  }
  result = result + (0.1 * Settings.temp_comp);
  return result;
}

char TempUnit(void)
{
  // SetOption8  - Switch between Celsius or Fahrenheit
  return (Settings.flag.temperature_conversion) ? D_UNIT_FAHRENHEIT[0] : D_UNIT_CELSIUS[0];
}

float ConvertHumidity(float h)
{
  float result = h;

  TasmotaGlobal.global_update = TasmotaGlobal.uptime;
  TasmotaGlobal.humidity = h;

  result = result + (0.1 * Settings.hum_comp);

  return result;
}

float CalcTempHumToDew(float t, float h)
{
  if (isnan(h) || isnan(t)) { return NAN; }

  if (Settings.flag.temperature_conversion) {                 // SetOption8 - Switch between Celsius or Fahrenheit
    t = (t - 32) / 1.8;                                       // Celsius
  }

  float gamma = TaylorLog(h / 100) + 17.62 * t / (243.5 + t);
  float result = (243.5 * gamma / (17.62 - gamma));

  if (Settings.flag.temperature_conversion) {                 // SetOption8 - Switch between Celsius or Fahrenheit
    result = result * 1.8 + 32;                               // Fahrenheit
  }
  return result;
}

float ConvertPressure(float p)
{
  float result = p;

  TasmotaGlobal.global_update = TasmotaGlobal.uptime;
  TasmotaGlobal.pressure_hpa = p;

  if (!isnan(p) && Settings.flag.pressure_conversion) {  // SetOption24 - Switch between hPa or mmHg pressure unit
    result = p * 0.75006375541921;                       // mmHg
  }
  return result;
}

float ConvertPressureForSeaLevel(float pressure)
{
  if (pressure == 0.0f)
    return pressure;

  return ConvertPressure((pressure / FastPrecisePow(1.0 - ((float)Settings.altitude / 44330.0f), 5.255f)) - 21.6f);
}

String PressureUnit(void)
{
  return (Settings.flag.pressure_conversion) ? String(D_UNIT_MILLIMETER_MERCURY) : String(D_UNIT_PRESSURE);
}

float ConvertSpeed(float s)
{
  // Entry in m/s
  return s * kSpeedConversionFactor[Settings.flag2.speed_conversion];
}

String SpeedUnit(void)
{
  char speed[8];
  return String(GetTextIndexed(speed, sizeof(speed), Settings.flag2.speed_conversion, kSpeedUnit));
}

void ResetGlobalValues(void)
{
  if ((TasmotaGlobal.uptime - TasmotaGlobal.global_update) > GLOBAL_VALUES_VALID) {  // Reset after 5 minutes
    TasmotaGlobal.global_update = 0;
    TasmotaGlobal.temperature_celsius = NAN;
    TasmotaGlobal.humidity = 0.0f;
    TasmotaGlobal.pressure_hpa = 0.0f;
  }
}

uint32_t SqrtInt(uint32_t num)
{
  if (num <= 1) {
    return num;
  }

  uint32_t x = num / 2;
  uint32_t y;
  do {
    y = (x + num / x) / 2;
    if (y >= x) {
      return x;
    }
    x = y;
  } while (true);
}

uint32_t RoundSqrtInt(uint32_t num)
{
  uint32_t s = SqrtInt(4 * num);
  if (s & 1) {
    s++;
  }
  return s / 2;
}

char* GetTextIndexed(char* destination, size_t destination_size, uint32_t index, const char* haystack)
{
  // Returns empty string if not found
  // Returns text of found
  char* write = destination;
  const char* read = haystack;

  index++;
  while (index--) {
    size_t size = destination_size -1;
    write = destination;
    char ch = '.';
    while ((ch != '\0') && (ch != '|')) {
      ch = pgm_read_byte(read++);
      if (size && (ch != '|'))  {
        *write++ = ch;
        size--;
      }
    }
    if (0 == ch) {
      if (index) {
        write = destination;
      }
      break;
    }
  }
  *write = '\0';
  return destination;
}

int GetCommandCode(char* destination, size_t destination_size, const char* needle, const char* haystack)
{
  // Returns -1 of not found
  // Returns index and command if found
  int result = -1;
  const char* read = haystack;
  char* write = destination;

  while (true) {
    result++;
    size_t size = destination_size -1;
    write = destination;
    char ch = '.';
    while ((ch != '\0') && (ch != '|')) {
      ch = pgm_read_byte(read++);
      if (size && (ch != '|'))  {
        *write++ = ch;
        size--;
      }
    }
    *write = '\0';
    if (!strcasecmp(needle, destination)) {
      break;
    }
    if (0 == ch) {
      result = -1;
      break;
    }
  }
  return result;
}

bool DecodeCommand(const char* haystack, void (* const MyCommand[])(void))
{
  GetTextIndexed(XdrvMailbox.command, CMDSZ, 0, haystack);  // Get prefix if available
  int prefix_length = strlen(XdrvMailbox.command);
  if (prefix_length) {
    char prefix[prefix_length +1];
    snprintf_P(prefix, sizeof(prefix), XdrvMailbox.topic);  // Copy prefix part only
    if (strcasecmp(prefix, XdrvMailbox.command)) {
      return false;                                         // Prefix not in command
    }
  }
  int command_code = GetCommandCode(XdrvMailbox.command + prefix_length, CMDSZ, XdrvMailbox.topic + prefix_length, haystack);
  if (command_code > 0) {                                   // Skip prefix
    XdrvMailbox.command_code = command_code -1;
    MyCommand[XdrvMailbox.command_code]();
    return true;
  }
  return false;
}

const char kOptions[] PROGMEM = "OFF|" D_OFF "|FALSE|" D_FALSE "|STOP|" D_STOP "|" D_CELSIUS "|"              // 0
                                "ON|" D_ON "|TRUE|" D_TRUE "|START|" D_START "|" D_FAHRENHEIT "|" D_USER "|"  // 1
                                "TOGGLE|" D_TOGGLE "|" D_ADMIN "|"                                            // 2
                                "BLINK|" D_BLINK "|"                                                          // 3
                                "BLINKOFF|" D_BLINKOFF "|"                                                    // 4
                                "ALL" ;                                                                       // 255

const uint8_t sNumbers[] PROGMEM = { 0,0,0,0,0,0,0,
                                     1,1,1,1,1,1,1,1,
                                     2,2,2,
                                     3,3,
                                     4,4,
                                     255 };

int GetStateNumber(char *state_text)
{
  char command[CMDSZ];
  int state_number = GetCommandCode(command, sizeof(command), state_text, kOptions);
  if (state_number >= 0) {
    state_number = pgm_read_byte(sNumbers + state_number);
  }
  return state_number;
}

String GetSerialConfig(void) {
  // Settings.serial_config layout
  // b000000xx - 5, 6, 7 or 8 data bits
  // b00000x00 - 1 or 2 stop bits
  // b000xx000 - None, Even or Odd parity

  const char kParity[] = "NEOI";

  char config[4];
  config[0] = '5' + (Settings.serial_config & 0x3);
  config[1] = kParity[(Settings.serial_config >> 3) & 0x3];
  config[2] = '1' + ((Settings.serial_config >> 2) & 0x1);
  config[3] = '\0';
  return String(config);
}

uint32_t GetSerialBaudrate(void) {
  return (Serial.baudRate() / 300) * 300;  // Fix ESP32 strange results like 115201
}

void SetSerialBegin(void) {
  TasmotaGlobal.baudrate = Settings.baudrate * 300;
  AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_SERIAL "Set to %s %d bit/s"), GetSerialConfig().c_str(), TasmotaGlobal.baudrate);
  Serial.flush();
#ifdef ESP8266
  Serial.begin(TasmotaGlobal.baudrate, (SerialConfig)pgm_read_byte(kTasmotaSerialConfig + Settings.serial_config));
#endif  // ESP8266
#ifdef ESP32
  delay(10);  // Allow time to cleanup queues - if not used hangs ESP32
  Serial.end();
  delay(10);  // Allow time to cleanup queues - if not used hangs ESP32
  uint32_t config = pgm_read_dword(kTasmotaSerialConfig + Settings.serial_config);
  Serial.begin(TasmotaGlobal.baudrate, config);
#endif  // ESP32
}

void SetSerialConfig(uint32_t serial_config) {
  if (serial_config > TS_SERIAL_8O2) {
    serial_config = TS_SERIAL_8N1;
  }
  if (serial_config != Settings.serial_config) {
    Settings.serial_config = serial_config;
    SetSerialBegin();
  }
}

void SetSerialBaudrate(uint32_t baudrate) {
  TasmotaGlobal.baudrate = baudrate;
  Settings.baudrate = TasmotaGlobal.baudrate / 300;
  if (GetSerialBaudrate() != TasmotaGlobal.baudrate) {
    SetSerialBegin();
  }
}

void SetSerial(uint32_t baudrate, uint32_t serial_config) {
  Settings.flag.mqtt_serial = 0;  // CMND_SERIALSEND and CMND_SERIALLOG
  Settings.serial_config = serial_config;
  TasmotaGlobal.baudrate = baudrate;
  Settings.baudrate = TasmotaGlobal.baudrate / 300;
  SetSeriallog(LOG_LEVEL_NONE);
  SetSerialBegin();
}

void ClaimSerial(void) {
  TasmotaGlobal.serial_local = true;
  AddLog_P(LOG_LEVEL_INFO, PSTR("SNS: Hardware Serial"));
  SetSeriallog(LOG_LEVEL_NONE);
  TasmotaGlobal.baudrate = GetSerialBaudrate();
  Settings.baudrate = TasmotaGlobal.baudrate / 300;
}

void SerialSendRaw(char *codes)
{
  char *p;
  char stemp[3];
  uint8_t code;

  int size = strlen(codes);

  while (size > 1) {
    strlcpy(stemp, codes, sizeof(stemp));
    code = strtol(stemp, &p, 16);
    Serial.write(code);
    size -= 2;
    codes += 2;
  }
}

// values is a comma-delimited string: e.g. "72,101,108,108,111,32,87,111,114,108,100,33,10"
void SerialSendDecimal(char *values)
{
  char *p;
  uint8_t code;
  for (char* str = strtok_r(values, ",", &p); str; str = strtok_r(nullptr, ",", &p)) {
    code = (uint8_t)atoi(str);
    Serial.write(code);
  }
}

uint32_t GetHash(const char *buffer, size_t size)
{
  uint32_t hash = 0;
  for (uint32_t i = 0; i <= size; i++) {
    hash += (uint8_t)*buffer++ * (i +1);
  }
  return hash;
}

void ShowSource(uint32_t source)
{
  if ((source > 0) && (source < SRC_MAX)) {
    char stemp1[20];
    AddLog_P(LOG_LEVEL_DEBUG, PSTR("SRC: %s"), GetTextIndexed(stemp1, sizeof(stemp1), source, kCommandSource));
  }
}

void WebHexCode(uint32_t i, const char* code)
{
  char scolor[10];

  strlcpy(scolor, code, sizeof(scolor));
  char* p = scolor;
  if ('#' == p[0]) { p++; }  // Skip

  if (3 == strlen(p)) {  // Convert 3 character to 6 character color code
    p[6] = p[3];  // \0
    p[5] = p[2];  // 3
    p[4] = p[2];  // 3
    p[3] = p[1];  // 2
    p[2] = p[1];  // 2
    p[1] = p[0];  // 1
  }

  uint32_t color = strtol(p, nullptr, 16);
/*
  if (3 == strlen(p)) {  // Convert 3 character to 6 character color code
    uint32_t w = ((color & 0xF00) << 8) | ((color & 0x0F0) << 4) | (color & 0x00F);  // 00010203
    color = w | (w << 4);                                                            // 00112233
  }
*/
  uint32_t j = sizeof(Settings.web_color) / 3;          // First area contains j = 18 colors
/*
  if (i < j) {
    Settings.web_color[i][0] = (color >> 16) & 0xFF;  // Red
    Settings.web_color[i][1] = (color >> 8) & 0xFF;   // Green
    Settings.web_color[i][2] = color & 0xFF;          // Blue
  } else {
    Settings.web_color2[i-j][0] = (color >> 16) & 0xFF;  // Red
    Settings.web_color2[i-j][1] = (color >> 8) & 0xFF;   // Green
    Settings.web_color2[i-j][2] = color & 0xFF;          // Blue
  }
*/
  if (i >= j) {
    // Calculate i to index in Settings.web_color2 - Dirty(!) but saves 128 bytes code
    i += ((((uint8_t*)&Settings.web_color2 - (uint8_t*)&Settings.web_color) / 3) - j);
  }
  Settings.web_color[i][0] = (color >> 16) & 0xFF;  // Red
  Settings.web_color[i][1] = (color >> 8) & 0xFF;   // Green
  Settings.web_color[i][2] = color & 0xFF;          // Blue
}

uint32_t WebColor(uint32_t i)
{
  uint32_t j = sizeof(Settings.web_color) / 3;          // First area contains j = 18 colors
/*
  uint32_t tcolor = (i<j)? (Settings.web_color[i][0] << 16) | (Settings.web_color[i][1] << 8) | Settings.web_color[i][2] :
                           (Settings.web_color2[i-j][0] << 16) | (Settings.web_color2[i-j][1] << 8) | Settings.web_color2[i-j][2];
*/
  if (i >= j) {
    // Calculate i to index in Settings.web_color2 - Dirty(!) but saves 128 bytes code
    i += ((((uint8_t*)&Settings.web_color2 - (uint8_t*)&Settings.web_color) / 3) - j);
  }
  uint32_t tcolor = (Settings.web_color[i][0] << 16) | (Settings.web_color[i][1] << 8) | Settings.web_color[i][2];

  return tcolor;
}

/*********************************************************************************************\
 * Response data handling
\*********************************************************************************************/

const uint16_t TIMESZ = 100;                   // Max number of characters in time string

char* ResponseGetTime(uint32_t format, char* time_str)
{
  switch (format) {
  case 1:
    snprintf_P(time_str, TIMESZ, PSTR("{\"" D_JSON_TIME "\":\"%s\",\"Epoch\":%u"), GetDateAndTime(DT_LOCAL).c_str(), UtcTime());
    break;
  case 2:
    snprintf_P(time_str, TIMESZ, PSTR("{\"" D_JSON_TIME "\":%u"), UtcTime());
    break;
  case 3:
    snprintf_P(time_str, TIMESZ, PSTR("{\"" D_JSON_TIME "\":\"%s\""), GetDateAndTime(DT_LOCAL_MILLIS).c_str());
    break;
  default:
    snprintf_P(time_str, TIMESZ, PSTR("{\"" D_JSON_TIME "\":\"%s\""), GetDateAndTime(DT_LOCAL).c_str());
  }
  return time_str;
}

void ResponseClear(void) {
  TasmotaGlobal.mqtt_data[0] = '\0';
}

int Response_P(const char* format, ...)        // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
  va_list args;
  va_start(args, format);
  int len = vsnprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), format, args);
  va_end(args);
  return len;
}

int ResponseTime_P(const char* format, ...)    // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
  va_list args;
  va_start(args, format);

  ResponseGetTime(Settings.flag2.time_format, TasmotaGlobal.mqtt_data);

  int mlen = strlen(TasmotaGlobal.mqtt_data);
  int len = vsnprintf_P(TasmotaGlobal.mqtt_data + mlen, sizeof(TasmotaGlobal.mqtt_data) - mlen, format, args);
  va_end(args);
  return len + mlen;
}

int ResponseAppend_P(const char* format, ...)  // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
  va_list args;
  va_start(args, format);
  int mlen = strlen(TasmotaGlobal.mqtt_data);
  int len = vsnprintf_P(TasmotaGlobal.mqtt_data + mlen, sizeof(TasmotaGlobal.mqtt_data) - mlen, format, args);
  va_end(args);
  return len + mlen;
}

int ResponseAppendTimeFormat(uint32_t format)
{
  char time_str[TIMESZ];
  return ResponseAppend_P(ResponseGetTime(format, time_str));
}

int ResponseAppendTime(void)
{
  return ResponseAppendTimeFormat(Settings.flag2.time_format);
}

int ResponseAppendTHD(float f_temperature, float f_humidity)
{
  char temperature[FLOATSZ];
  dtostrfd(f_temperature, Settings.flag2.temperature_resolution, temperature);
  char humidity[FLOATSZ];
  dtostrfd(f_humidity, Settings.flag2.humidity_resolution, humidity);
  char dewpoint[FLOATSZ];
  dtostrfd(CalcTempHumToDew(f_temperature, f_humidity), Settings.flag2.temperature_resolution, dewpoint);

  return ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%s,\"" D_JSON_HUMIDITY "\":%s,\"" D_JSON_DEWPOINT "\":%s"), temperature, humidity, dewpoint);
}

int ResponseJsonEnd(void)
{
  return ResponseAppend_P(PSTR("}"));
}

int ResponseJsonEndEnd(void)
{
  return ResponseAppend_P(PSTR("}}"));
}

/*********************************************************************************************\
 * GPIO Module and Template management
\*********************************************************************************************/

#ifdef ESP8266
uint16_t GpioConvert(uint8_t gpio) {
  if (gpio >= ARRAY_SIZE(kGpioConvert)) {
    return AGPIO(GPIO_USER);
  }
  return pgm_read_word(kGpioConvert + gpio);
}

uint16_t Adc0Convert(uint8_t adc0) {
  if (adc0 > 7) {
    return AGPIO(GPIO_USER);
  }
  else if (0 == adc0) {
    return GPIO_NONE;
  }
  return AGPIO(GPIO_ADC_INPUT + adc0 -1);
}

void TemplateConvert(uint8_t template8[], uint16_t template16[]) {
  for (uint32_t i = 0; i < (sizeof(mytmplt) / 2) -2; i++) {
    template16[i] = GpioConvert(template8[i]);
  }
  template16[(sizeof(mytmplt) / 2) -2] = Adc0Convert(template8[sizeof(mytmplt8285) -1]);

//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("FNC: TemplateConvert"));
//  AddLogBuffer(LOG_LEVEL_DEBUG, template8, sizeof(mytmplt8285));
//  AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t*)template16, sizeof(mytmplt) / 2, 2);
}

void ConvertGpios(void) {
  if (Settings.gpio16_converted != 0xF5A0) {
    // Convert 8-bit user template
    TemplateConvert((uint8_t*)&Settings.ex_user_template8, (uint16_t*)&Settings.user_template);

    for (uint32_t i = 0; i < sizeof(Settings.ex_my_gp8.io); i++) {
      Settings.my_gp.io[i] = GpioConvert(Settings.ex_my_gp8.io[i]);
    }
    Settings.my_gp.io[(sizeof(myio) / 2) -1] = Adc0Convert(Settings.ex_my_adc0);
    Settings.gpio16_converted = 0xF5A0;

//    AddLog_P(LOG_LEVEL_DEBUG, PSTR("FNC: ConvertGpios"));
//    AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t *)&Settings.ex_my_gp8.io, sizeof(myio8));
//    AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t *)&Settings.my_gp.io, sizeof(myio) / 2, 2);
  }
}

/*
void DumpConvertTable(void) {
  bool jsflg = false;
  uint32_t lines = 1;
  for (uint32_t i = 0; i < ARRAY_SIZE(kGpioConvert); i++) {
    uint32_t data = pgm_read_word(kGpioConvert + i);
    if (!jsflg) {
      Response_P(PSTR("{\"GPIOConversion%d\":{"), lines);
    } else {
      ResponseAppend_P(PSTR(","));
    }
    jsflg = true;
    if ((ResponseAppend_P(PSTR("\"%d\":\"%d\""), i, data) > (LOGSZ - TOPSZ)) || (i == ARRAY_SIZE(kGpioConvert) -1)) {
      ResponseJsonEndEnd();
      MqttPublishPrefixTopic_P(RESULT_OR_STAT, XdrvMailbox.command);
      jsflg = false;
      lines++;
    }
  }
  for (uint32_t i = 0; i < ARRAY_SIZE(kAdcNiceList); i++) {
    uint32_t data = pgm_read_word(kAdcNiceList + i);
    if (!jsflg) {
      Response_P(PSTR("{\"ADC0Conversion%d\":{"), lines);
    } else {
      ResponseAppend_P(PSTR(","));
    }
    jsflg = true;
    if ((ResponseAppend_P(PSTR("\"%d\":\"%d\""), i, data) > (LOGSZ - TOPSZ)) || (i == ARRAY_SIZE(kAdcNiceList) -1)) {
      ResponseJsonEndEnd();
      MqttPublishPrefixTopic_P(RESULT_OR_STAT, XdrvMailbox.command);
      jsflg = false;
      lines++;
    }
  }
  ResponseClear();
}
*/
#endif  // ESP8266

uint32_t ICACHE_RAM_ATTR Pin(uint32_t gpio, uint32_t index = 0);
uint32_t ICACHE_RAM_ATTR Pin(uint32_t gpio, uint32_t index) {
  uint16_t real_gpio = gpio << 5;
  uint16_t mask = 0xFFE0;
  if (index < GPIO_ANY) {
    real_gpio += index;
    mask = 0xFFFF;
  }
  for (uint32_t i = 0; i < ARRAY_SIZE(TasmotaGlobal.gpio_pin); i++) {
    if ((TasmotaGlobal.gpio_pin[i] & mask) == real_gpio) {
      return i;              // Pin number configured for gpio
    }
  }
  return 99;                 // No pin used for gpio
}

bool PinUsed(uint32_t gpio, uint32_t index = 0);
bool PinUsed(uint32_t gpio, uint32_t index) {
  return (Pin(gpio, index) < 99);
}

uint32_t GetPin(uint32_t lpin) {
  if (lpin < ARRAY_SIZE(TasmotaGlobal.gpio_pin)) {
    return TasmotaGlobal.gpio_pin[lpin];
  } else {
    return GPIO_NONE;
  }
}

void SetPin(uint32_t lpin, uint32_t gpio) {
  TasmotaGlobal.gpio_pin[lpin] = gpio;
}

void DigitalWrite(uint32_t gpio_pin, uint32_t index, uint32_t state)
{
  if (PinUsed(gpio_pin, index)) {
    digitalWrite(Pin(gpio_pin, index), state &1);
  }
}

uint8_t ModuleNr(void)
{
  // 0    = User module (255)
  // 1 up = Template module 0 up
  return (USER_MODULE == Settings.module) ? 0 : Settings.module +1;
}

bool ValidTemplateModule(uint32_t index)
{
  for (uint32_t i = 0; i < sizeof(kModuleNiceList); i++) {
    if (index == pgm_read_byte(kModuleNiceList + i)) {
      return true;
    }
  }
  return false;
}

bool ValidModule(uint32_t index)
{
  if (index == USER_MODULE) { return true; }
  return ValidTemplateModule(index);
}

bool ValidTemplate(const char *search) {
  char template_name[strlen(SettingsText(SET_TEMPLATE_NAME)) +1];
  char search_name[strlen(search) +1];

  LowerCase(template_name, SettingsText(SET_TEMPLATE_NAME));
  LowerCase(search_name, search);

  return (strstr(template_name, search_name) != nullptr);
}

String AnyModuleName(uint32_t index)
{
  if (USER_MODULE == index) {
    return String(SettingsText(SET_TEMPLATE_NAME));
  } else {
    char name[TOPSZ];
    return String(GetTextIndexed(name, sizeof(name), index, kModuleNames));
  }
}

String ModuleName(void)
{
  return AnyModuleName(Settings.module);
}

#ifdef ESP8266
void GetInternalTemplate(void* ptr, uint32_t module, uint32_t option) {
  uint8_t module_template = pgm_read_byte(kModuleTemplateList + module);

//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("DBG: Template %d, Option %d"), module_template, option);

  // template8 = GPIO 0,1,2,3,4,5,9,10,12,13,14,15,16,Adc
  uint8_t template8[sizeof(mytmplt8285)] = { GPIO_NONE };
  if (module_template < TMP_WEMOS) {
    memcpy_P(&template8, &kModules8266[module_template], 6);
    memcpy_P(&template8[8], &kModules8266[module_template].gp.io[6], 6);
  } else {
    memcpy_P(&template8, &kModules8285[module_template - TMP_WEMOS], sizeof(template8));
  }

//  AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t *)&template8, sizeof(mytmplt8285));

  // template16  = GPIO 0,1,2,3,4,5,9,10,12,13,14,15,16,Adc,Flg
  uint16_t template16[(sizeof(mytmplt) / 2)] = { GPIO_NONE };
  TemplateConvert(template8, template16);

  uint32_t index = 0;
  uint32_t size = sizeof(mycfgio);      // template16[module_template].gp
  switch (option) {
    case 2: {
      index = (sizeof(mytmplt) / 2) -1; // template16[module_template].flag
      size = 2;
      break;
    }
    case 3: {
      size = sizeof(mytmplt);           // template16[module_template]
      break;
    }
  }
  memcpy(ptr, &template16[index], size);

//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("FNC: GetInternalTemplate option %d"), option);
//  AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t *)ptr, size / 2, 2);
}
#endif  // ESP8266

void TemplateGpios(myio *gp)
{
  uint16_t *dest = (uint16_t *)gp;
  uint16_t src[ARRAY_SIZE(Settings.user_template.gp.io)];

  memset(dest, GPIO_NONE, sizeof(myio));
  if (USER_MODULE == Settings.module) {
    memcpy(&src, &Settings.user_template.gp, sizeof(mycfgio));
  } else {
#ifdef ESP8266
    GetInternalTemplate(&src, Settings.module, 1);
#endif  // ESP8266
#ifdef ESP32
    memcpy_P(&src, &kModules.gp, sizeof(mycfgio));
#endif  // ESP32
  }
  // 11 85 00 85 85 00 00 00 15 38 85 00 00 81

//  AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t *)&src, sizeof(mycfgio));

  uint32_t j = 0;
  for (uint32_t i = 0; i < ARRAY_SIZE(Settings.user_template.gp.io); i++) {
    if (6 == i) { j = 9; }
    if (8 == i) { j = 12; }
    dest[j] = src[i];
    j++;
  }
  // 11 85 00 85 85 00 00 00 00 00 00 00 15 38 85 00 00 81

//  AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t *)gp, sizeof(myio));
}

gpio_flag ModuleFlag(void)
{
  gpio_flag flag;

  if (USER_MODULE == Settings.module) {
    flag = Settings.user_template.flag;
  } else {
#ifdef ESP8266
    GetInternalTemplate(&flag, Settings.module, 2);
#endif  // ESP8266
#ifdef ESP32
    memcpy_P(&flag, &kModules.flag, sizeof(gpio_flag));
#endif  // ESP32
  }

  return flag;
}

void ModuleDefault(uint32_t module)
{
  if (USER_MODULE == module) { module = WEMOS; }  // Generic
  Settings.user_template_base = module;
  char name[TOPSZ];
  SettingsUpdateText(SET_TEMPLATE_NAME, GetTextIndexed(name, sizeof(name), module, kModuleNames));
#ifdef ESP8266
  GetInternalTemplate(&Settings.user_template, module, 3);
#endif  // ESP8266
#ifdef ESP32
  memcpy_P(&Settings.user_template, &kModules, sizeof(mytmplt));
#endif  // ESP32
}

void SetModuleType(void)
{
  TasmotaGlobal.module_type = (USER_MODULE == Settings.module) ? Settings.user_template_base : Settings.module;
}

bool FlashPin(uint32_t pin)
{
  return (((pin > 5) && (pin < 9)) || (11 == pin));
}

uint32_t ValidPin(uint32_t pin, uint32_t gpio)
{
  if (FlashPin(pin)) {
    return GPIO_NONE;    // Disable flash pins GPIO6, GPIO7, GPIO8 and GPIO11
  }

//  if (!TasmotaGlobal.is_8285 && !Settings.flag3.user_esp8285_enable) {  // SetOption51 - Enable ESP8285 user GPIO's
  if ((WEMOS == Settings.module) && !Settings.flag3.user_esp8285_enable) {  // SetOption51 - Enable ESP8285 user GPIO's
    if ((9 == pin) || (10 == pin)) {
      return GPIO_NONE;  // Disable possible flash GPIO9 and GPIO10
    }
  }

  return gpio;
}

bool ValidGPIO(uint32_t pin, uint32_t gpio)
{
#ifdef ESP8266
#ifdef USE_ADC_VCC
  if (ADC0_PIN == pin) { return false; }  // ADC0 = GPIO17
#endif
#endif
  return (GPIO_USER == ValidPin(pin, BGPIO(gpio)));  // Only allow GPIO_USER pins
}

bool JsonTemplate(char* dataBuf)
{
  // Old: {"NAME":"Shelly 2.5","GPIO":[56,0,17,0,21,83,0,0,6,82,5,22,156],"FLAG":2,"BASE":18}
  // New: {"NAME":"Shelly 2.5","GPIO":[320,0,32,0,224,193,0,0,640,192,608,225,3456,4736],"FLAG":0,"BASE":18}

//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("TPL: |%s|"), dataBuf);

  if (strlen(dataBuf) < 9) { return false; }  // Workaround exception if empty JSON like {} - Needs checks

  JsonParser parser((char*) dataBuf);
  JsonParserObject root = parser.getRootObject();
  if (!root) { return false; }

  // All parameters are optional allowing for partial changes
  JsonParserToken val = root[PSTR(D_JSON_NAME)];
  if (val) {
    SettingsUpdateText(SET_TEMPLATE_NAME, val.getStr());
  }
  JsonParserArray arr = root[PSTR(D_JSON_GPIO)];
  if (arr) {
#ifdef ESP8266
    bool old_template = false;
    uint8_t template8[sizeof(mytmplt8285)] = { GPIO_NONE };
    if (13 == arr.size()) {  // Possible old template
      uint32_t gpio = 0;
      for (uint32_t i = 0; i < ARRAY_SIZE(template8) -1; i++) {
        gpio = arr[i].getUInt();
        if (gpio > 255) {    // New templates might have values above 255
          break;
        }
        template8[i] = gpio;
      }
      old_template = (gpio < 256);
    }
    if (old_template) {

      AddLog_P(LOG_LEVEL_DEBUG, PSTR("TPL: Converting template ..."));

      val = root[PSTR(D_JSON_FLAG)];
      if (val) {
        template8[ARRAY_SIZE(template8) -1] = val.getUInt() & 0x0F;
      }
      TemplateConvert(template8, Settings.user_template.gp.io);
      Settings.user_template.flag.data = 0;
    } else {
#endif
      for (uint32_t i = 0; i < ARRAY_SIZE(Settings.user_template.gp.io); i++) {
        JsonParserToken val = arr[i];
        if (!val) { break; }
        uint16_t gpio = val.getUInt();
        if (gpio == (AGPIO(GPIO_NONE) +1)) {
          gpio = AGPIO(GPIO_USER);
        }
        Settings.user_template.gp.io[i] = gpio;
      }
      val = root[PSTR(D_JSON_FLAG)];
      if (val) {
        Settings.user_template.flag.data = val.getUInt();
      }
    }
#ifdef ESP8266
  }
#endif
  val = root[PSTR(D_JSON_BASE)];
  if (val) {
    uint32_t base = val.getUInt();
    if ((0 == base) || !ValidTemplateModule(base -1)) { base = 18; }
    Settings.user_template_base = base -1;  // Default WEMOS
  }

//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("TPL: Converted"));
//  AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t*)&Settings.user_template, sizeof(Settings.user_template) / 2, 2);

  return true;
}

void TemplateJson(void)
{
//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("TPL: Show"));
//  AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t*)&Settings.user_template, sizeof(Settings.user_template) / 2, 2);

  Response_P(PSTR("{\"" D_JSON_NAME "\":\"%s\",\"" D_JSON_GPIO "\":["), SettingsText(SET_TEMPLATE_NAME));
  for (uint32_t i = 0; i < ARRAY_SIZE(Settings.user_template.gp.io); i++) {
    uint16_t gpio = Settings.user_template.gp.io[i];
    if (gpio == AGPIO(GPIO_USER)) {
      gpio = AGPIO(GPIO_NONE) +1;
    }
    ResponseAppend_P(PSTR("%s%d"), (i>0)?",":"", gpio);
  }
  ResponseAppend_P(PSTR("],\"" D_JSON_FLAG "\":%d,\"" D_JSON_BASE "\":%d}"), Settings.user_template.flag, Settings.user_template_base +1);
}

/*********************************************************************************************\
 * Sleep aware time scheduler functions borrowed from ESPEasy
\*********************************************************************************************/

inline int32_t TimeDifference(uint32_t prev, uint32_t next)
{
  return ((int32_t) (next - prev));
}

int32_t TimePassedSince(uint32_t timestamp)
{
  // Compute the number of milliSeconds passed since timestamp given.
  // Note: value can be negative if the timestamp has not yet been reached.
  return TimeDifference(timestamp, millis());
}

bool TimeReached(uint32_t timer)
{
  // Check if a certain timeout has been reached.
  const long passed = TimePassedSince(timer);
  return (passed >= 0);
}

void SetNextTimeInterval(uint32_t& timer, const uint32_t step)
{
  timer += step;
  const long passed = TimePassedSince(timer);
  if (passed < 0) { return; }   // Event has not yet happened, which is fine.
  if (static_cast<unsigned long>(passed) > step) {
    // No need to keep running behind, start again.
    timer = millis() + step;
    return;
  }
  // Try to get in sync again.
  timer = millis() + (step - passed);
}

int32_t TimePassedSinceUsec(uint32_t timestamp)
{
  return TimeDifference(timestamp, micros());
}

bool TimeReachedUsec(uint32_t timer)
{
  // Check if a certain timeout has been reached.
  const long passed = TimePassedSinceUsec(timer);
  return (passed >= 0);
}

/*********************************************************************************************\
 * Basic I2C routines
\*********************************************************************************************/

#ifdef USE_I2C
const uint8_t I2C_RETRY_COUNTER = 3;

uint32_t i2c_active[4] = { 0 };
uint32_t i2c_buffer = 0;

bool I2cValidRead(uint8_t addr, uint8_t reg, uint8_t size)
{
  uint8_t retry = I2C_RETRY_COUNTER;
  bool status = false;

  i2c_buffer = 0;
  while (!status && retry) {
    Wire.beginTransmission(addr);                       // start transmission to device
    Wire.write(reg);                                    // sends register address to read from
    if (0 == Wire.endTransmission(false)) {             // Try to become I2C Master, send data and collect bytes, keep master status for next request...
      Wire.requestFrom((int)addr, (int)size);           // send data n-bytes read
      if (Wire.available() == size) {
        for (uint32_t i = 0; i < size; i++) {
          i2c_buffer = i2c_buffer << 8 | Wire.read();   // receive DATA
        }
        status = true;
      }
    }
    retry--;
  }
  if (!retry) Wire.endTransmission();
  return status;
}

bool I2cValidRead8(uint8_t *data, uint8_t addr, uint8_t reg)
{
  bool status = I2cValidRead(addr, reg, 1);
  *data = (uint8_t)i2c_buffer;
  return status;
}

bool I2cValidRead16(uint16_t *data, uint8_t addr, uint8_t reg)
{
  bool status = I2cValidRead(addr, reg, 2);
  *data = (uint16_t)i2c_buffer;
  return status;
}

bool I2cValidReadS16(int16_t *data, uint8_t addr, uint8_t reg)
{
  bool status = I2cValidRead(addr, reg, 2);
  *data = (int16_t)i2c_buffer;
  return status;
}

bool I2cValidRead16LE(uint16_t *data, uint8_t addr, uint8_t reg)
{
  uint16_t ldata;
  bool status = I2cValidRead16(&ldata, addr, reg);
  *data = (ldata >> 8) | (ldata << 8);
  return status;
}

bool I2cValidReadS16_LE(int16_t *data, uint8_t addr, uint8_t reg)
{
  uint16_t ldata;
  bool status = I2cValidRead16LE(&ldata, addr, reg);
  *data = (int16_t)ldata;
  return status;
}

bool I2cValidRead24(int32_t *data, uint8_t addr, uint8_t reg)
{
  bool status = I2cValidRead(addr, reg, 3);
  *data = i2c_buffer;
  return status;
}

uint8_t I2cRead8(uint8_t addr, uint8_t reg)
{
  I2cValidRead(addr, reg, 1);
  return (uint8_t)i2c_buffer;
}

uint16_t I2cRead16(uint8_t addr, uint8_t reg)
{
  I2cValidRead(addr, reg, 2);
  return (uint16_t)i2c_buffer;
}

int16_t I2cReadS16(uint8_t addr, uint8_t reg)
{
  I2cValidRead(addr, reg, 2);
  return (int16_t)i2c_buffer;
}

uint16_t I2cRead16LE(uint8_t addr, uint8_t reg)
{
  I2cValidRead(addr, reg, 2);
  uint16_t temp = (uint16_t)i2c_buffer;
  return (temp >> 8) | (temp << 8);
}

int16_t I2cReadS16_LE(uint8_t addr, uint8_t reg)
{
  return (int16_t)I2cRead16LE(addr, reg);
}

int32_t I2cRead24(uint8_t addr, uint8_t reg)
{
  I2cValidRead(addr, reg, 3);
  return i2c_buffer;
}

bool I2cWrite(uint8_t addr, uint8_t reg, uint32_t val, uint8_t size)
{
  uint8_t x = I2C_RETRY_COUNTER;

  do {
    Wire.beginTransmission((uint8_t)addr);              // start transmission to device
    Wire.write(reg);                                    // sends register address to write to
    uint8_t bytes = size;
    while (bytes--) {
      Wire.write((val >> (8 * bytes)) & 0xFF);          // write data
    }
    x--;
  } while (Wire.endTransmission(true) != 0 && x != 0);  // end transmission
  return (x);
}

bool I2cWrite8(uint8_t addr, uint8_t reg, uint16_t val)
{
   return I2cWrite(addr, reg, val, 1);
}

bool I2cWrite16(uint8_t addr, uint8_t reg, uint16_t val)
{
   return I2cWrite(addr, reg, val, 2);
}

int8_t I2cReadBuffer(uint8_t addr, uint8_t reg, uint8_t *reg_data, uint16_t len)
{
  Wire.beginTransmission((uint8_t)addr);
  Wire.write((uint8_t)reg);
  Wire.endTransmission();
  if (len != Wire.requestFrom((uint8_t)addr, (uint8_t)len)) {
    return 1;
  }
  while (len--) {
    *reg_data = (uint8_t)Wire.read();
    reg_data++;
  }
  return 0;
}

int8_t I2cWriteBuffer(uint8_t addr, uint8_t reg, uint8_t *reg_data, uint16_t len)
{
  Wire.beginTransmission((uint8_t)addr);
  Wire.write((uint8_t)reg);
  while (len--) {
    Wire.write(*reg_data);
    reg_data++;
  }
  Wire.endTransmission();
  return 0;
}

void I2cScan(char *devs, unsigned int devs_len)
{
  // Return error codes defined in twi.h and core_esp8266_si2c.c
  // I2C_OK                      0
  // I2C_SCL_HELD_LOW            1 = SCL held low by another device, no procedure available to recover
  // I2C_SCL_HELD_LOW_AFTER_READ 2 = I2C bus error. SCL held low beyond client clock stretch time
  // I2C_SDA_HELD_LOW            3 = I2C bus error. SDA line held low by client/another_master after n bits
  // I2C_SDA_HELD_LOW_AFTER_INIT 4 = line busy. SDA again held low by another device. 2nd master?

  uint8_t error = 0;
  uint8_t address = 0;
  uint8_t any = 0;

  snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_DEVICES_FOUND_AT));
  for (address = 1; address <= 127; address++) {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    if (0 == error) {
      any = 1;
      snprintf_P(devs, devs_len, PSTR("%s 0x%02x"), devs, address);
    }
    else if (error != 2) {  // Seems to happen anyway using this scan
      any = 2;
      snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"Error %d at 0x%02x"), error, address);
      break;
    }
  }
  if (any) {
    strncat(devs, "\"}", devs_len - strlen(devs) -1);
  }
  else {
    snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_NO_DEVICES_FOUND "\"}"));
  }
}

void I2cResetActive(uint32_t addr, uint32_t count = 1)
{
  addr &= 0x7F;         // Max I2C address is 127
  count &= 0x7F;        // Max 4 x 32 bits available
  while (count-- && (addr < 128)) {
    i2c_active[addr / 32] &= ~(1 << (addr % 32));
    addr++;
  }
//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("I2C: Active %08X,%08X,%08X,%08X"), i2c_active[0], i2c_active[1], i2c_active[2], i2c_active[3]);
}

void I2cSetActive(uint32_t addr, uint32_t count = 1)
{
  addr &= 0x7F;         // Max I2C address is 127
  count &= 0x7F;        // Max 4 x 32 bits available
  while (count-- && (addr < 128)) {
    i2c_active[addr / 32] |= (1 << (addr % 32));
    addr++;
  }
//  AddLog_P(LOG_LEVEL_DEBUG, PSTR("I2C: Active %08X,%08X,%08X,%08X"), i2c_active[0], i2c_active[1], i2c_active[2], i2c_active[3]);
}

void I2cSetActiveFound(uint32_t addr, const char *types)
{
  I2cSetActive(addr);
  AddLog_P(LOG_LEVEL_INFO, S_LOG_I2C_FOUND_AT, types, addr);
}

bool I2cActive(uint32_t addr)
{
  addr &= 0x7F;         // Max I2C address is 127
  if (i2c_active[addr / 32] & (1 << (addr % 32))) {
    return true;
  }
  return false;
}

bool I2cSetDevice(uint32_t addr)
{
  addr &= 0x7F;         // Max I2C address is 127
  if (I2cActive(addr)) {
    return false;       // If already active report as not present;
  }
  Wire.beginTransmission((uint8_t)addr);
  return (0 == Wire.endTransmission());
}
#endif  // USE_I2C

/*********************************************************************************************\
 * Syslog
 *
 * Example:
 *   AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_LOG "Any value %d"), value);
 *
\*********************************************************************************************/

void SetSeriallog(uint32_t loglevel)
{
  Settings.seriallog_level = loglevel;
  TasmotaGlobal.seriallog_level = loglevel;
  TasmotaGlobal.seriallog_timer = 0;
}

void SetSyslog(uint32_t loglevel)
{
  Settings.syslog_level = loglevel;
  TasmotaGlobal.syslog_level = loglevel;
  TasmotaGlobal.syslog_timer = 0;
}

#ifdef USE_WEBSERVER
void GetLog(uint32_t idx, char** entry_pp, size_t* len_p)
{
  char* entry_p = nullptr;
  size_t len = 0;

  if (idx) {
    char* it = TasmotaGlobal.web_log;
    do {
      uint32_t cur_idx = *it;
      it++;
      size_t tmp = strchrspn(it, '\1');
      tmp++;                             // Skip terminating '\1'
      if (cur_idx == idx) {              // Found the requested entry
        len = tmp;
        entry_p = it;
        break;
      }
      it += tmp;
    } while (it < TasmotaGlobal.web_log + WEB_LOG_SIZE && *it != '\0');
  }
  *entry_pp = entry_p;
  *len_p = len;
}
#endif  // USE_WEBSERVER

void Syslog(void)
{
  // Destroys TasmotaGlobal.log_data

  uint32_t current_hash = GetHash(SettingsText(SET_SYSLOG_HOST), strlen(SettingsText(SET_SYSLOG_HOST)));
  if (syslog_host_hash != current_hash) {
    syslog_host_hash = current_hash;
    WiFi.hostByName(SettingsText(SET_SYSLOG_HOST), syslog_host_addr);  // If sleep enabled this might result in exception so try to do it once using hash
  }
  if (PortUdp.beginPacket(syslog_host_addr, Settings.syslog_port)) {
    char syslog_preamble[64];  // Hostname + Id
    snprintf_P(syslog_preamble, sizeof(syslog_preamble), PSTR("%s ESP-"), NetworkHostname());
    memmove(TasmotaGlobal.log_data + strlen(syslog_preamble), TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data) - strlen(syslog_preamble));
    TasmotaGlobal.log_data[sizeof(TasmotaGlobal.log_data) -1] = '\0';
    memcpy(TasmotaGlobal.log_data, syslog_preamble, strlen(syslog_preamble));
    PortUdp_write(TasmotaGlobal.log_data, strlen(TasmotaGlobal.log_data));
    PortUdp.endPacket();
    delay(1);  // Add time for UDP handling (#5512)
  } else {
    TasmotaGlobal.syslog_level = 0;
    TasmotaGlobal.syslog_timer = SYSLOG_TIMER;
    AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SYSLOG_HOST_NOT_FOUND ". " D_RETRY_IN " %d " D_UNIT_SECOND), SYSLOG_TIMER);
  }
}

void AddLog(uint32_t loglevel)
{
  char mxtime[10];  // "13:45:21 "
  snprintf_P(mxtime, sizeof(mxtime), PSTR("%02d" D_HOUR_MINUTE_SEPARATOR "%02d" D_MINUTE_SECOND_SEPARATOR "%02d "), RtcTime.hour, RtcTime.minute, RtcTime.second);

  if ((loglevel <= TasmotaGlobal.seriallog_level) &&
      (TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) {
    Serial.printf("%s%s\r\n", mxtime, TasmotaGlobal.log_data);
  }
#ifdef USE_WEBSERVER
  if (Settings.webserver &&
     (loglevel <= Settings.weblog_level) &&
     (TasmotaGlobal.masterlog_level <= Settings.weblog_level)) {
    // Delimited, zero-terminated buffer of log lines.
    // Each entry has this format: [index][log data]['\1']
    TasmotaGlobal.web_log_index &= 0xFF;
    if (!TasmotaGlobal.web_log_index) {
      TasmotaGlobal.web_log_index++;       // Index 0 is not allowed as it is the end of char string
    }
    while (TasmotaGlobal.web_log_index == TasmotaGlobal.web_log[0] ||  // If log already holds the next index, remove it
           strlen(TasmotaGlobal.web_log) + strlen(TasmotaGlobal.log_data) + 13 > WEB_LOG_SIZE)  // 13 = web_log_index + mxtime + '\1' + '\0'
    {
      char* it = TasmotaGlobal.web_log;
      it++;                                // Skip web_log_index
      it += strchrspn(it, '\1');           // Skip log line
      it++;                                // Skip delimiting "\1"
      memmove(TasmotaGlobal.web_log, it, WEB_LOG_SIZE -(it-TasmotaGlobal.web_log));  // Move buffer forward to remove oldest log line
    }
    snprintf_P(TasmotaGlobal.web_log, sizeof(TasmotaGlobal.web_log), PSTR("%s%c%s%s\1"), TasmotaGlobal.web_log, TasmotaGlobal.web_log_index++, mxtime, TasmotaGlobal.log_data);
    TasmotaGlobal.web_log_index &= 0xFF;
    if (!TasmotaGlobal.web_log_index) {
      TasmotaGlobal.web_log_index++;       // Index 0 is not allowed as it is the end of char string
    }
  }
#endif  // USE_WEBSERVER
  if (Settings.flag.mqtt_enabled &&        // SetOption3 - Enable MQTT
      !TasmotaGlobal.global_state.mqtt_down &&
      (loglevel <= Settings.mqttlog_level) &&
      (TasmotaGlobal.masterlog_level <= Settings.mqttlog_level)) { MqttPublishLogging(mxtime); }

  if (!TasmotaGlobal.global_state.network_down &&
      (loglevel <= TasmotaGlobal.syslog_level) &&
      (TasmotaGlobal.masterlog_level <= TasmotaGlobal.syslog_level)) { Syslog(); }

  TasmotaGlobal.prepped_loglevel = 0;
}

void PrepLog_P(uint32_t loglevel, PGM_P formatP, ...)
{
  va_list arg;
  va_start(arg, formatP);
  vsnprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), formatP, arg);
  va_end(arg);

  TasmotaGlobal.prepped_loglevel = loglevel;
}

void AddLog_P(uint32_t loglevel, PGM_P formatP, ...)
{
  if (TasmotaGlobal.prepped_loglevel) {
    AddLog(TasmotaGlobal.prepped_loglevel);
  }

  va_list arg;
  va_start(arg, formatP);
  vsnprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), formatP, arg);
  va_end(arg);

  AddLog(loglevel);
}

void AddLog_Debug(PGM_P formatP, ...)
{
  va_list arg;
  va_start(arg, formatP);
  vsnprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), formatP, arg);
  va_end(arg);

  AddLog(LOG_LEVEL_DEBUG);
}

void AddLogBuffer(uint32_t loglevel, uint8_t *buffer, uint32_t count)
{
/*
  snprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), PSTR("DMP:"));
  for (uint32_t i = 0; i < count; i++) {
    snprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), PSTR("%s %02X"), TasmotaGlobal.log_data, *(buffer++));
  }
  AddLog(loglevel);
*/
/*
  strcpy_P(TasmotaGlobal.log_data, PSTR("DMP: "));
  ToHex_P(buffer, count, TasmotaGlobal.log_data + strlen(TasmotaGlobal.log_data), sizeof(TasmotaGlobal.log_data) - strlen(TasmotaGlobal.log_data), ' ');
  AddLog(loglevel);
*/
  char hex_char[(count * 3) + 2];
  AddLog_P(loglevel, PSTR("DMP: %s"), ToHex_P(buffer, count, hex_char, sizeof(hex_char), ' '));
}

void AddLogSerial(uint32_t loglevel)
{
  AddLogBuffer(loglevel, (uint8_t*)TasmotaGlobal.serial_in_buffer, TasmotaGlobal.serial_in_byte_counter);
}

void AddLogMissed(const char *sensor, uint32_t misses)
{
  AddLog_P(LOG_LEVEL_DEBUG, PSTR("SNS: %s missed %d"), sensor, SENSOR_MAX_MISS - misses);
}

void AddLogBufferSize(uint32_t loglevel, uint8_t *buffer, uint32_t count, uint32_t size) {
  snprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), PSTR("DMP:"));
  for (uint32_t i = 0; i < count; i++) {
    if (1 ==  size) {  // uint8_t
      snprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), PSTR("%s %02X"), TasmotaGlobal.log_data, *(buffer));
    } else {           // uint16_t
      snprintf_P(TasmotaGlobal.log_data, sizeof(TasmotaGlobal.log_data), PSTR("%s %02X%02X"), TasmotaGlobal.log_data, *(buffer +1), *(buffer));
    }
    buffer += size;
  }
  AddLog(loglevel);
}

/*********************************************************************************************\
 * Uncompress static PROGMEM strings
\*********************************************************************************************/

#ifdef USE_UNISHOX_COMPRESSION

#include <unishox.h>

Unishox compressor;

String Decompress(const char * compressed, size_t uncompressed_size) {
  String content("");

  uncompressed_size += 2;    // take a security margin

  // We use a nasty trick here. To avoid allocating twice the buffer,
  // we first extend the buffer of the String object to the target size (maybe overshooting by 7 bytes)
  // then we decompress in this buffer,
  // and finally assign the raw string to the String, which happens to work: String uses memmove(), so overlapping works
  content.reserve(uncompressed_size);
  char * buffer = content.begin();

  int32_t len = compressor.unishox_decompress(compressed, strlen_P(compressed), buffer, uncompressed_size);
  if (len > 0) {
    buffer[len] = 0;    // terminate string with NULL
    content = buffer;         // copy in place
  }
  return content;
}

#endif // USE_UNISHOX_COMPRESSION

/*********************************************************************************************\
 * High entropy hardware random generator
 * Thanks to DigitalAlchemist
\*********************************************************************************************/
// Based on code from https://raw.githubusercontent.com/espressif/esp-idf/master/components/esp32/hw_random.c
uint32_t HwRandom(void) {
#if ESP8266
  // https://web.archive.org/web/20160922031242/http://esp8266-re.foogod.com/wiki/Random_Number_Generator
  #define _RAND_ADDR 0x3FF20E44UL
#endif  // ESP8266
#ifdef ESP32
  #define _RAND_ADDR 0x3FF75144UL
#endif  // ESP32
  static uint32_t last_ccount = 0;
  uint32_t ccount;
  uint32_t result = 0;
  do {
    ccount = ESP.getCycleCount();
    result ^= *(volatile uint32_t *)_RAND_ADDR;
  } while (ccount - last_ccount < 64);
  last_ccount = ccount;
  return result ^ *(volatile uint32_t *)_RAND_ADDR;
#undef _RAND_ADDR
}
