/*
  support.ino - support for Tasmota

  Copyright (C) 2021  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/>.
*/

extern "C" {
extern struct rst_info resetInfo;
}

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

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

#ifdef ESP8266
#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() IRAM_ATTR;
#endif  // USE_WS2812_DMA

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(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(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
    // ESP32: Guru Meditation Error: Core  0 panic'ed (LoadProhibited). Exception was unhandled.
    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;
}

#else  // Anything except ESP8266

void OsWatchInit(void) {}
void OsWatchLoop(void) {}
bool OsWatchBlockedLoop(void) {
  return false;
}

#endif  // ESP8266

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 (OsWatchBlockedLoop()) {
    char buff[32];
    strncpy_P(buff, PSTR(D_JSON_BLOCKED_LOOP), sizeof(buff));
    return String(buff);
  } else {
    return ESP_getResetReason();
  }
}

#ifdef ESP32
/*********************************************************************************************\
 * ESP32 AutoMutex
\*********************************************************************************************/

//////////////////////////////////////////
// automutex.
// create a mute in your driver with:
// void *mutex = nullptr;
//
// then protect any function with
// TasAutoMutex m(&mutex, "somename");
// - mutex is automatically initialised if not already intialised.
// - it will be automagically released when the function is over.
// - the same thread can take multiple times (recursive).
// - advanced options m.give() and m.take() allow you fine control within a function.
// - if take=false at creat, it will not be initially taken.
// - name is used in serial log of mutex deadlock.
// - maxWait in ticks is how long it will wait before failing in a deadlock scenario (and then emitting on serial)
class TasAutoMutex {
  SemaphoreHandle_t mutex;
  bool taken;
  int maxWait;
  const char *name;
  public:
    TasAutoMutex(SemaphoreHandle_t* mutex, const char *name = "", int maxWait = 40, bool take=true);
    ~TasAutoMutex();
    void give();
    void take();
    static void init(SemaphoreHandle_t* ptr);
};
//////////////////////////////////////////

TasAutoMutex::TasAutoMutex(SemaphoreHandle_t*mutex, const char *name, int maxWait, bool take) {
  if (mutex) {
    if (!(*mutex)){
      TasAutoMutex::init(mutex);
    }
    this->mutex = *mutex;
    this->maxWait = maxWait;
    this->name = name;
    if (take) {
      this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait);
//      if (!this->taken){
//        Serial.printf("\r\nMutexfail %s\r\n", this->name);
//      }
    }
  } else {
    this->mutex = (SemaphoreHandle_t)nullptr;
  }
}

TasAutoMutex::~TasAutoMutex() {
  if (this->mutex) {
    if (this->taken) {
      xSemaphoreGiveRecursive(this->mutex);
      this->taken = false;
    }
  }
}

void TasAutoMutex::init(SemaphoreHandle_t* ptr) {
  SemaphoreHandle_t mutex = xSemaphoreCreateRecursiveMutex();
  (*ptr) = mutex;
  // needed, else for ESP8266 as we will initialis more than once in logging
//  (*ptr) = (void *) 1;
}

void TasAutoMutex::give() {
  if (this->mutex) {
    if (this->taken) {
      xSemaphoreGiveRecursive(this->mutex);
      this->taken= false;
    }
  }
}

void TasAutoMutex::take() {
  if (this->mutex) {
    if (!this->taken) {
      this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait);
//      if (!this->taken){
//        Serial.printf("\r\nMutexfail %s\r\n", this->name);
//      }
    }
  }
}

#endif  // ESP32


/*********************************************************************************************\
 * 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;
}

uint32_t ArgC(void) {
  return (XdrvMailbox.data_len > 0) ? ChrCount(XdrvMailbox.data, ",") +1 : 0;
}

// Function to return a substring defined by a delimiter at an index
char* subStr(char* dest, char* str, const char *delim, int index) {
  char* write = dest;
  char* read = str;
  char ch = '.';

  while (index && (ch != '\0')) {
    ch = *read++;
    if (strchr(delim, ch)) {
      index--;
      if (index) { write = dest; }
    } else {
      *write++ = ch;
    }
  }
  *write = '\0';
  dest = Trim(dest);
  return dest;
}

char* ArgV(char* dest, int index) {
  return subStr(dest, XdrvMailbox.data, ",", index);
}

uint32_t ArgVul(uint32_t *args, uint32_t count) {
  uint32_t argc = ArgC();
  if (argc > count) { argc = count; }
  count = argc;
  if (argc) {
    char argument[XdrvMailbox.data_len];
    for (uint32_t i = 0; i < argc; i++) {
      if (strlen(ArgV(argument, i +1))) {
        args[i] = strtoul(argument, nullptr, 0);
      } else {
        count--;
      }
    }
  }
  return count;
}

uint32_t ParseParameters(uint32_t count, uint32_t *params) {
  // Destroys XdrvMailbox.data
  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;
}

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.0f; }

  while ((*pt != '\0') && isspace(*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* 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_P(s, PSTR("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* ReplaceChar(char* p, char find, char replace) {
  char* write = (char*)p;
  char* read = (char*)p;
  char ch = '.';

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

char* ReplaceCommaWithDot(char* p) {
  return ReplaceChar(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;
}

bool StrCaseStr_P(const char* source, const char* search) {
  char case_source[strlen_P(source) +1];
  UpperCase_P(case_source, source);
  char case_search[strlen_P(search) +1];
  UpperCase_P(case_search, search);
  return (strstr(case_source, case_search) != nullptr);
}

bool IsNumeric(const char* value) {
  // Test for characters '-.0123456789'
  char *digit = (char*)value;
  while (isdigit(*digit) || *digit == '.' || *digit == '-') { digit++; }
  return (*digit == '\0');
}

char* Trim(char* p) {
  // Remove leading and trailing tab, \n, \v, \f, \r and space
  if (*p != '\0') {
    while ((*p != '\0') && isspace(*p)) { p++; }  // Trim leading spaces
    char* q = p + strlen(p) -1;
    while ((q >= p) && isspace(*q)) { q--; }   // Trim trailing spaces
    q++;
    *q = '\0';
  }
  return p;
}

String HexToString(uint8_t* data, uint32_t length) {
  if (!data || !length) { return ""; }

  uint32_t len = (length < 16) ? length : 16;
  char hex_data[32];
  ToHex_P((const unsigned char*)data, len, hex_data, sizeof(hex_data));
  String result = hex_data;
  result += F(" [");
  for (uint32_t i = 0; i < len; i++) {
    result += (isprint(data[i])) ? (char)data[i] : ' ';
  }
  result += F("]");
  if (length > len) {
    result += F(" ...");
  }
  return result;
}

String UrlEncode(const String& text) {
  const char hex[] = "0123456789ABCDEF";

	String encoded = "";
	int len = text.length();
	int i = 0;
	while (i < len)	{
		char decodedChar = text.charAt(i++);
/*
    if (('a' <= decodedChar && decodedChar <= 'z') ||
        ('A' <= decodedChar && decodedChar <= 'Z') ||
        ('0' <= decodedChar && decodedChar <= '9') ||
        ('=' == decodedChar)) {
      encoded += decodedChar;
		} else {
      encoded += '%';
			encoded += hex[decodedChar >> 4];
			encoded += hex[decodedChar & 0xF];
    }
*/
    if ((' ' == decodedChar) || ('+' == decodedChar)) {
      encoded += '%';
			encoded += hex[decodedChar >> 4];
			encoded += hex[decodedChar & 0xF];
    } else {
      encoded += decodedChar;
    }

	}
	return encoded;
}

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)
{
  IPAddress ip_address;
  return ip_address.fromString(str);
}

bool ParseIPv4(uint32_t* addr, const char* str_p)
{
  uint8_t *part = (uint8_t*)addr;
  uint8_t i;
  char str_r[strlen_P(str_p)+1];
  char * str = &str_r[0];
  strcpy_P(str, str_p);

  *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);
}

bool NewerVersion(char* version_str) {
  // Function to parse & check if version_str is newer than our currently installed version.
  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);
}

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

  if (!isnan(c) && Settings->flag.temperature_conversion) {    // SetOption8 - Switch between Celsius or Fahrenheit
    result = c * 1.8f + 32;                                    // Fahrenheit
  }
  result = result + (0.1f * 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.8f;                                  // Celsius
  }
  result = result + (0.1f * Settings->temp_comp);
  return result;
}

void UpdateGlobalTemperature(float c) {
  TasmotaGlobal.global_update = TasmotaGlobal.uptime;
  TasmotaGlobal.temperature_celsius = c;
}

float ConvertTemp(float c) {
  UpdateGlobalTemperature(c);

  return ConvertTempToFahrenheit(c);
}

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.1f * 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.8f;                                        // Celsius
  }

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

  if (Settings->flag.temperature_conversion) {                 // SetOption8 - Switch between Celsius or Fahrenheit
    result = result * 1.8f + 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
    if (Settings->flag5.mm_vs_inch) {                          // SetOption139 - Switch between mmHg or inHg pressure unit
//      result = p * 0.02952998016471;                           // inHg
      result = p * 0.0295299f;                                 // inHg (double to float saves 16 bytes!)
    } else {
//      result = p * 0.75006375541921;                           // mmHg
      result = p * 0.7500637f;                                 // mmHg (double to float saves 16 bytes!)
    }
  }
  return result;
}

float ConvertPressureForSeaLevel(float pressure) {
  if (pressure == 0.0f) {
    return pressure;
  }
  return ConvertPressure((pressure / FastPrecisePowf(1.0f - ((float)Settings->altitude / 44330.0f), 5.255f)) - 21.6f);
}

const char kPressureUnit[] PROGMEM = D_UNIT_PRESSURE "|" D_UNIT_MILLIMETER_MERCURY "|" D_UNIT_INCH_MERCURY;

String PressureUnit(void) {
  uint32_t index = (Settings->flag.pressure_conversion) ? Settings->flag5.mm_vs_inch +1 : 0;
  char text[8];
  return String(GetTextIndexed(text, sizeof(text), index, kPressureUnit));
}

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

String SpeedUnit(void) {
  char text[8];
  return String(GetTextIndexed(text, sizeof(text), 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), const uint8_t *synonyms = nullptr);
bool DecodeCommand(const char* haystack, void (* const MyCommand[])(void), const uint8_t *synonyms) {
  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
    }
  }
  size_t syn_count = synonyms ? pgm_read_byte(synonyms) : 0;
  int command_code = GetCommandCode(XdrvMailbox.command + prefix_length, CMDSZ, XdrvMailbox.topic + prefix_length, haystack);
  if (command_code > 0) {                                   // Skip prefix
    if (command_code > syn_count) {
      // We passed the synonyms zone, it's a regular command
      XdrvMailbox.command_code = command_code - 1 - syn_count;
      MyCommand[XdrvMailbox.command_code]();
    } else {
      // We have a SetOption synonym
      XdrvMailbox.index = pgm_read_byte(synonyms + command_code);
      CmndSetoptionBase(0);
    }
    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(const 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;
}

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(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;
}

char* ResponseData(void) {
#ifdef MQTT_DATA_STRING
  return (char*)TasmotaGlobal.mqtt_data.c_str();
#else
  return TasmotaGlobal.mqtt_data;
#endif
}

uint32_t ResponseSize(void) {
#ifdef MQTT_DATA_STRING
  return MAX_LOGSZ;                            // Arbitratry max length satisfying full log entry
#else
  return sizeof(TasmotaGlobal.mqtt_data);
#endif
}

uint32_t ResponseLength(void) {
#ifdef MQTT_DATA_STRING
  return TasmotaGlobal.mqtt_data.length();
#else
  return strlen(TasmotaGlobal.mqtt_data);
#endif
}

void ResponseClear(void) {
  // Reset string length to zero
#ifdef MQTT_DATA_STRING
  TasmotaGlobal.mqtt_data = "";
//  TasmotaGlobal.mqtt_data = (const char*) nullptr;  // Doesn't work on ESP32 as strlen() (in MqttPublishPayload) will fail (for obvious reasons)
#else
  TasmotaGlobal.mqtt_data[0] = '\0';
#endif
}

void ResponseJsonStart(void) {
  // Insert a JSON start bracket {
#ifdef MQTT_DATA_STRING
  TasmotaGlobal.mqtt_data.setCharAt(0,'{');
#else
  TasmotaGlobal.mqtt_data[0] = '{';
#endif
}

int Response_P(const char* format, ...)        // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
#ifdef MQTT_DATA_STRING
  va_list arg;
  va_start(arg, format);
  char* mqtt_data = ext_vsnprintf_malloc_P(format, arg);
  va_end(arg);
  if (mqtt_data != nullptr) {
    TasmotaGlobal.mqtt_data = mqtt_data;
    free(mqtt_data);
  } else {
    TasmotaGlobal.mqtt_data = "";
  }
  return TasmotaGlobal.mqtt_data.length();
#else
  va_list args;
  va_start(args, format);
  int len = ext_vsnprintf_P(TasmotaGlobal.mqtt_data, ResponseSize(), format, args);
  va_end(args);
  return len;
#endif
}

int ResponseTime_P(const char* format, ...)    // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
#ifdef MQTT_DATA_STRING
  char timestr[100];
  TasmotaGlobal.mqtt_data = ResponseGetTime(Settings->flag2.time_format, timestr);

  va_list arg;
  va_start(arg, format);
  char* mqtt_data = ext_vsnprintf_malloc_P(format, arg);
  va_end(arg);
  if (mqtt_data != nullptr) {
    TasmotaGlobal.mqtt_data += mqtt_data;
    free(mqtt_data);
  }
  return TasmotaGlobal.mqtt_data.length();
#else
  va_list args;
  va_start(args, format);

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

  int mlen = ResponseLength();
  int len = ext_vsnprintf_P(TasmotaGlobal.mqtt_data + mlen, ResponseSize() - mlen, format, args);
  va_end(args);
  return len + mlen;
#endif
}

int ResponseAppend_P(const char* format, ...)  // Content send snprintf_P char data
{
  // This uses char strings. Be aware of sending %% if % is needed
#ifdef MQTT_DATA_STRING
  va_list arg;
  va_start(arg, format);
  char* mqtt_data = ext_vsnprintf_malloc_P(format, arg);
  va_end(arg);
  if (mqtt_data != nullptr) {
    TasmotaGlobal.mqtt_data += mqtt_data;
    free(mqtt_data);
  }
  return TasmotaGlobal.mqtt_data.length();
#else
  va_list args;
  va_start(args, format);
  int mlen = ResponseLength();
  int len = ext_vsnprintf_P(TasmotaGlobal.mqtt_data + mlen, ResponseSize() - mlen, format, args);
  va_end(args);
  return len + mlen;
#endif
}

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)
{
  float dewpoint = CalcTempHumToDew(f_temperature, f_humidity);
  return ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%*_f,\"" D_JSON_HUMIDITY "\":%*_f,\"" D_JSON_DEWPOINT "\":%*_f"),
                          Settings->flag2.temperature_resolution, &f_temperature,
                          Settings->flag2.humidity_resolution, &f_humidity,
                          Settings->flag2.temperature_resolution, &dewpoint);
}

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

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

bool ResponseContains_P(const char* needle) {
/*
#ifdef MQTT_DATA_STRING
  return (strstr_P(TasmotaGlobal.mqtt_data.c_str(), needle) != nullptr);
#else
  return (strstr_P(TasmotaGlobal.mqtt_data, needle) != nullptr);
#endif
*/
  return (strstr_P(ResponseData(), needle) != nullptr);
}

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

#ifdef ESP8266
uint16_t GpioConvert(uint8_t gpio) {
  if (gpio >= nitems(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]);
}

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;
  }
}
#endif  // ESP8266

int IRAM_ATTR Pin(uint32_t gpio, uint32_t index = 0);
int IRAM_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 < nitems(TasmotaGlobal.gpio_pin); i++) {
    if ((TasmotaGlobal.gpio_pin[i] & mask) == real_gpio) {
      return i;              // Pin number configured for gpio
    }
  }
  return -1;                 // 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) >= 0);
}

uint32_t GetPin(uint32_t lpin) {
  if (lpin < nitems(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) {
  static uint32_t pinmode_init[2] = { 0 };       // Pins 0 to 63 !!!

  if (PinUsed(gpio_pin, index)) {
    uint32_t pin = Pin(gpio_pin, index) & 0x3F;  // Fix possible overflow over 63 gpios
    if (!bitRead(pinmode_init[pin / 32], pin % 32)) {
      bitSet(pinmode_init[pin / 32], pin % 32);
      pinMode(pin, OUTPUT);
    }
    digitalWrite(pin, 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;
}

uint32_t ModuleTemplate(uint32_t module) {
  uint32_t i = 0;
  for (i = 0; i < sizeof(kModuleNiceList); i++) {
    if (module == pgm_read_byte(kModuleNiceList + i)) {
      break;
    }
  }
  if (i == sizeof(kModuleNiceList)) { i = 0; }
  return i;
}

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);
*/
  return StrCaseStr_P(SettingsText(SET_TEMPLATE_NAME), search);
}

String AnyModuleName(uint32_t index)
{
  if (USER_MODULE == index) {
    return String(SettingsText(SET_TEMPLATE_NAME));
  } else {
#ifdef ESP32
    index = ModuleTemplate(index);
#endif
    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(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(LOG_LEVEL_DEBUG, PSTR("FNC: GetInternalTemplate option %d"), option);
//  AddLogBufferSize(LOG_LEVEL_DEBUG, (uint8_t *)ptr, size / 2, 2);
}
#endif  // ESP8266

#ifdef CONFIG_IDF_TARGET_ESP32
// Conversion table from gpio template to physical gpio
const uint8_t Esp32TemplateToPhy[MAX_USER_PINS] = { ESP32_TEMPLATE_TO_PHY };
#endif // CONFIG_IDF_TARGET_ESP32

void TemplateGpios(myio *gp)
{
  uint16_t *dest = (uint16_t *)gp;
  uint16_t src[nitems(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[ModuleTemplate(Settings->module)].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));

  // Expand template to physical GPIO array, j=phy_GPIO, i=template_GPIO
  uint32_t j = 0;
  for (uint32_t i = 0; i < nitems(Settings->user_template.gp.io); i++) {
#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32C3
    dest[i] = src[i];
#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
    if (22 == i) { j = 33; }    // skip 22-32
    dest[j] = src[i];
    j++;
#elif defined(CONFIG_IDF_TARGET_ESP32)
    dest[Esp32TemplateToPhy[i]] = src[i];
#else // ESP8266
    if (6 == i) { j = 9; }
    if (8 == i) { j = 12; }
    dest[j] = src[i];
    j++;
#endif
  }
  // 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[ModuleTemplate(Settings->module)].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;

#ifdef ESP32
  module = ModuleTemplate(module);
#endif

  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[module], sizeof(mytmplt));
#endif  // ESP32
}

void SetModuleType(void)
{
  TasmotaGlobal.module_type = (USER_MODULE == Settings->module) ? Settings->user_template_base : Settings->module;
#ifdef ESP32
  if (TasmotaGlobal.emulated_module_type) {
    TasmotaGlobal.module_type = TasmotaGlobal.emulated_module_type;
  }
#endif
}

bool FlashPin(uint32_t pin)
{
#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32C3
  return (pin > 10) && (pin < 18);        // ESP32C3 has GPIOs 11-17 reserved for Flash
#elif defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
  return (pin > 21) && (pin < 33);        // ESP32S2 skip 22-32
#elif defined(CONFIG_IDF_TARGET_ESP32)
  return (pin >= 28) && (pin <= 31);      // ESP21 skip 28-31
#else // ESP8266
  return (((pin > 5) && (pin < 9)) || (11 == pin));
#endif
}

bool RedPin(uint32_t pin) // pin may be dangerous to change, display in RED in template console
{
#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32C3
  return false;     // no red pin on ESP32C3
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
  return false;     // no red pin on ESP32S3
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
  return (33<=pin) && (37>=pin);  // ESP32S3: GPIOs 33..37 are usually used for PSRAM
#elif defined(CONFIG_IDF_TARGET_ESP32)  // red pins are 6-11 for original ESP32, other models like PICO are not impacted if flash pins are condfigured
  // PICO can also have 16/17/18/23 not available
  return ((6<=pin) && (11>=pin)) || (16==pin) || (17==pin);  // TODO adapt depending on the exact type of ESP32
#else // ESP8266
  return (9==pin)||(10==pin);
#endif
}

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

#if defined(CONFIG_IDF_TARGET_ESP32C3)
// ignore
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
// ignore
#elif defined(CONFIG_IDF_TARGET_ESP32)
// ignore
#else // not ESP32C3 and not ESP32S2
  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
    }
  }
#endif

  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 ValidSpiPinUsed(uint32_t gpio) {
  // ESP8266: If SPI pin selected chk if it's not one of the three Hardware SPI pins (12..14)
  bool result = false;
  if (PinUsed(gpio)) {
    int pin = Pin(gpio);
    result = ((pin < 12) || (pin > 14));
  }
  return result;
}

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(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 < nitems(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(LOG_LEVEL_DEBUG, PSTR("TPL: Converting template ..."));

      val = root[PSTR(D_JSON_FLAG)];
      if (val) {
        template8[nitems(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 < nitems(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
  }

  val = root[PSTR(D_JSON_CMND)];
  if (val) {
    if ((USER_MODULE == Settings->module) || StrCaseStr_P(val.getStr(), PSTR(D_CMND_MODULE " 0"))) {  // Only execute if current module = USER_MODULE = this template
      char* backup_data = XdrvMailbox.data;
      XdrvMailbox.data = (char*)val.getStr();   // Backlog commands
      ReplaceChar(XdrvMailbox.data, '|', ';');  // Support '|' as command separator for JSON backwards compatibility
      uint32_t backup_data_len = XdrvMailbox.data_len;
      XdrvMailbox.data_len = 1;                 // Any data
      uint32_t backup_index = XdrvMailbox.index;
      XdrvMailbox.index = 0;                    // Backlog0 - no delay
      CmndBacklog();
      XdrvMailbox.index = backup_index;
      XdrvMailbox.data_len = backup_data_len;
      XdrvMailbox.data = backup_data;
    }
  }

//  AddLog(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(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 < nitems(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);
}

#if ( defined(USE_SCRIPT) && defined(SUPPORT_MQTT_EVENT) ) || defined (USE_DT_VARS)

/*********************************************************************************************\
 * Parse json paylod with path
\*********************************************************************************************/
// parser object, source keys, delimiter, float result or NULL, string result or NULL, string size
// return 1 if numeric 2 if string, else 0 = not found
uint32_t JsonParsePath(JsonParserObject *jobj, const char *spath, char delim, float *nres, char *sres, uint32_t slen) {
  uint32_t res = 0;
  const char *cp = spath;
#ifdef DEBUG_JSON_PARSE_PATH
  AddLog(LOG_LEVEL_INFO, PSTR("JSON: parsing json key: %s from json: %s"), cp, jpath);
#endif
  JsonParserObject obj = *jobj;
  JsonParserObject lastobj = obj;
  char selem[64];
  uint8_t aindex = 0;
  String value = "";
  while (1) {
    // read next element
    for (uint32_t sp=0; sp<sizeof(selem)-1; sp++) {
      if (!*cp || *cp==delim) {
        selem[sp] = 0;
        cp++;
        break;
      }
      selem[sp] = *cp++;
    }
#ifdef DEBUG_JSON_PARSE_PATH
    AddLog(LOG_LEVEL_INFO, PSTR("JSON: cmp current key: %s"), selem);
#endif
    // check for array
    char *sp = strchr(selem,'[');
    if (sp) {
      *sp = 0;
      aindex = atoi(sp+1);
    }

    // now check element
    obj = obj[selem];
    if (!obj.isValid()) {
#ifdef DEBUG_JSON_PARSE_PATH
      AddLog(LOG_LEVEL_INFO, PSTR("JSON: obj invalid: %s"), selem);
#endif
      JsonParserToken tok = lastobj[selem];
      if (tok.isValid()) {
        if (tok.isArray()) {
          JsonParserArray array = JsonParserArray(tok);
          value = array[aindex].getStr();
          if (array.isNum()) {
            if (nres) *nres=tok.getFloat();
            res = 1;
          } else {
            res = 2;
          }
        } else {
          value = tok.getStr();
          if (tok.isNum()) {
            if (nres) *nres=tok.getFloat();
            res = 1;
          } else {
            res = 2;
          }
        }

      }
#ifdef DEBUG_JSON_PARSE_PATH
      AddLog(LOG_LEVEL_INFO, PSTR("JSON: token invalid: %s"), selem);
#endif
      break;
    }
    if (obj.isObject()) {
      lastobj = obj;
      continue;
    }
    if (!*cp) break;
  }
  if (sres) {
    strlcpy(sres,value.c_str(), slen);
  }
  return res;

}

#endif // USE_SCRIPT

/*********************************************************************************************\
 * Serial
\*********************************************************************************************/

String GetSerialConfig(uint8_t serial_config) {
  // 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 static char kParity[] PROGMEM = "NEOI";

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

String GetSerialConfig(void) {
  return GetSerialConfig(Settings->serial_config);
}

int8_t ParseSerialConfig(const char *pstr)
{
  if (strlen(pstr) < 3)
    return -1;

  int8_t serial_config = (uint8_t)atoi(pstr);
  if (serial_config < 5 || serial_config > 8)
    return -1;
  serial_config -= 5;

  char parity = (pstr[1] & 0xdf);
  if ('E' == parity) {
    serial_config += 0x08;                         // Even parity
  }
  else if ('O' == parity) {
    serial_config += 0x10;                         // Odd parity
  }
  else if ('N' != parity) {
    return -1;
  }

  if ('2' == pstr[2]) {
    serial_config += 0x04;                         // Stop bits 2
  }
  else if ('1' != pstr[2]) {
    return -1;
  }

  return serial_config;
}

uint32_t ConvertSerialConfig(uint8_t serial_config) {
#ifdef ESP8266
  return (uint32_t)pgm_read_byte(kTasmotaSerialConfig + serial_config);
#elif defined(ESP32)
  return (uint32_t)pgm_read_dword(kTasmotaSerialConfig + serial_config);
#else
  #error "platform not supported"
#endif
}

// workaround disabled 05.11.2021 solved with https://github.com/espressif/arduino-esp32/pull/5549
//#if defined(ESP32) && CONFIG_IDF_TARGET_ESP32C3
// temporary workaround, see https://github.com/espressif/arduino-esp32/issues/5287
//#include <driver/uart.h>
//uint32_t GetSerialBaudrate(void) {
//  uint32_t br;
//  uart_get_baudrate(0, &br);
//  return (br / 300) * 300;  // Fix ESP32 strange results like 115201
//}
//#else
uint32_t GetSerialBaudrate(void) {
  return (Serial.baudRate() / 300) * 300;  // Fix ESP32 strange results like 115201
}
//#endif

#ifdef ESP8266
void SetSerialSwap(void) {
  if ((15 == Pin(GPIO_TXD)) && (13 == Pin(GPIO_RXD))) {
    Serial.flush();
    Serial.swap();
    AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_SERIAL "Serial pins swapped to alternate"));
  }
}
#endif

void SetSerialBegin(void) {
  TasmotaGlobal.baudrate = Settings->baudrate * 300;
  AddLog(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)ConvertSerialConfig(Settings->serial_config));
  SetSerialSwap();
#endif  // ESP8266
#ifdef ESP32
#ifdef ARDUINO_USB_CDC_ON_BOOT
//  Serial.end();
//  Serial.begin();
  // Above sequence ends in "Exception":5,"Reason":"Load access fault"
  AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_SERIAL "HWCDC supports 115200 bit/s only"));
#else
  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
  Serial.begin(TasmotaGlobal.baudrate, ConvertSerialConfig(Settings->serial_config));
#endif  // Not ARDUINO_USB_CDC_ON_BOOT
#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(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);
  }
}

/*********************************************************************************************/

uint8_t Bcd2Dec(uint8_t n) {
  return n - 6 * (n >> 4);
}

uint8_t Dec2Bcd(uint8_t n) {
  return n + 6 * (n / 10);
}

/*********************************************************************************************/

uint8_t TasShiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder) {
  uint8_t value = 0;

  for (uint32_t i = 0; i < 8; ++i) {
    digitalWrite(clockPin, HIGH);
#ifdef ESP32
    delayMicroseconds(1);
#endif
    if(bitOrder == LSBFIRST) {
      value |= digitalRead(dataPin) << i;
    } else {
      value |= digitalRead(dataPin) << (7 - i);
    }
    digitalWrite(clockPin, LOW);
#ifdef ESP32
    delayMicroseconds(1);
#endif
  }
  return value;
}

void TasShiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val) {
  for (uint32_t i = 0; i < 8; i++) {
    if(bitOrder == LSBFIRST) {
      digitalWrite(dataPin, !!(val & (1 << i)));
    } else {
      digitalWrite(dataPin, !!(val & (1 << (7 - i))));
    }
    digitalWrite(clockPin, HIGH);
#ifdef ESP32
    delayMicroseconds(1);
#endif
    digitalWrite(clockPin, LOW);
#ifdef ESP32
    delayMicroseconds(1);
#endif
  }
}

/*********************************************************************************************\
 * 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 I2cBegin(int sda, int scl, uint32_t frequency = 100000);
bool I2cBegin(int sda, int scl, uint32_t frequency) {
  bool result = true;
#ifdef ESP8266
  Wire.begin(sda, scl);
#endif
#ifdef ESP32
#if ESP_IDF_VERSION_MAJOR > 3  // Core 2.x uses a different I2C library
  static bool reinit = false;
  if (reinit) { Wire.end(); }
#endif  // ESP_IDF_VERSION_MAJOR > 3
  result = Wire.begin(sda, scl, frequency);
#if ESP_IDF_VERSION_MAJOR > 3  // Core 2.x uses a different I2C library
  reinit = result;
#endif  // ESP_IDF_VERSION_MAJOR > 3
#endif
//  AddLog(LOG_LEVEL_DEBUG, PSTR("I2C: Bus1 %d"), result);
  return result;
}

#ifdef ESP32
bool I2c2Begin(int sda, int scl, uint32_t frequency = 100000);
bool I2c2Begin(int sda, int scl, uint32_t frequency) {
  bool result = Wire1.begin(sda, scl, frequency);
//  AddLog(LOG_LEVEL_DEBUG, PSTR("I2C: Bus2 %d"), result);
  return result;
}

bool I2cValidRead(uint8_t addr, uint8_t reg, uint8_t size, uint32_t bus = 0);
bool I2cValidRead(uint8_t addr, uint8_t reg, uint8_t size, uint32_t bus)
#else
bool I2cValidRead(uint8_t addr, uint8_t reg, uint8_t size)
#endif
{
  uint8_t retry = I2C_RETRY_COUNTER;
  bool status = false;
#ifdef ESP32
  if (!TasmotaGlobal.i2c_enabled_2) { bus = 0; }
  TwoWire & myWire = (bus == 0) ? Wire : Wire1;
#else
  TwoWire & myWire = Wire;
#endif

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

#ifdef ESP32
bool I2cWrite(uint8_t addr, uint8_t reg, uint32_t val, uint8_t size, uint32_t bus = 0);
bool I2cWrite(uint8_t addr, uint8_t reg, uint32_t val, uint8_t size, uint32_t bus)
#else
bool I2cWrite(uint8_t addr, uint8_t reg, uint32_t val, uint8_t size)
#endif
{
  uint8_t x = I2C_RETRY_COUNTER;

#ifdef ESP32
  if (!TasmotaGlobal.i2c_enabled_2) { bus = 0; }
  TwoWire & myWire = (bus == 0) ? Wire : Wire1;
#else
  TwoWire & myWire = Wire;
#endif

  do {
    myWire.beginTransmission((uint8_t)addr);              // start transmission to device
    myWire.write(reg);                                    // sends register address to write to
    uint8_t bytes = size;
    while (bytes--) {
      myWire.write((val >> (8 * bytes)) & 0xFF);          // write data
    }
    x--;
  } while (myWire.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(uint32_t bus = 0);
void I2cScan(uint32_t bus) {
  // 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;

  Response_P(PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_DEVICES_FOUND_AT));
  for (address = 1; address <= 127; address++) {
#ifdef ESP32
    if (!TasmotaGlobal.i2c_enabled_2) { bus = 0; }
    TwoWire & myWire = (bus == 0) ? Wire : Wire1;
#else
    TwoWire & myWire = Wire;
#endif
    myWire.beginTransmission(address);
    error = myWire.endTransmission();
    if (0 == error) {
      any = 1;
      ResponseAppend_P(PSTR(" 0x%02x"), address);
    }
    else if (error != 2) {  // Seems to happen anyway using this scan
      any = 2;
      Response_P(PSTR("{\"" D_CMND_I2CSCAN "\":\"Error %d at 0x%02x"), error, address);
      break;
    }
  }
  if (any) {
    ResponseAppend_P(PSTR("\"}"));
  }
  else {
    Response_P(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(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(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, uint32_t bus = 0);
void I2cSetActiveFound(uint32_t addr, const char *types, uint32_t bus) {
  I2cSetActive(addr);
#ifdef ESP32
  if (0 == bus) {
    AddLog(LOG_LEVEL_INFO, S_LOG_I2C_FOUND_AT, types, addr);
  } else {
    AddLog(LOG_LEVEL_INFO, S_LOG_I2C_FOUND_AT_PORT, types, addr, bus);
  }
#else
  AddLog(LOG_LEVEL_INFO, S_LOG_I2C_FOUND_AT, types, addr);
#endif // ESP32
}

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, uint32_t bus = 0);
bool I2cSetDevice(uint32_t addr, uint32_t bus) {
#ifdef ESP32
  if (!TasmotaGlobal.i2c_enabled_2) { bus = 0; }
  TwoWire & myWire = (bus == 0) ? Wire : Wire1;
#else
  TwoWire & myWire = Wire;
#endif
  addr &= 0x7F;         // Max I2C address is 127
  if (I2cActive(addr)) {
    return false;       // If already active report as not present;
  }
  myWire.beginTransmission((uint8_t)addr);
  return (0 == myWire.endTransmission());
}
#endif  // USE_I2C

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

void SetTasConlog(uint32_t loglevel) {
  Settings->seriallog_level = loglevel;
  TasmotaGlobal.seriallog_level = loglevel;
  TasmotaGlobal.seriallog_timer = 0;
}

void SetSeriallog(uint32_t loglevel) {
#ifdef ESP32
  if (tasconsole_serial) {
#endif  // ESP32

  SetTasConlog(loglevel);

#ifdef ESP32
  }
#endif  // ESP32
}

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

void SyslogAsync(bool refresh) {
  static IPAddress syslog_host_addr;      // Syslog host IP address
  static uint32_t syslog_host_hash = 0;   // Syslog host name hash
  static uint32_t index = 1;

  if (!TasmotaGlobal.syslog_level || TasmotaGlobal.global_state.network_down) { return; }
  if (refresh && !NeedLogRefresh(TasmotaGlobal.syslog_level, index)) { return; }

  char* line;
  size_t len;
  while (GetLog(TasmotaGlobal.syslog_level, &index, &line, &len)) {
    // 00:00:02.096 HTP: Web server active on wemos5 with IP address 192.168.2.172
    //              HTP: Web server active on wemos5 with IP address 192.168.2.172
    uint32_t mxtime = strchr(line, ' ') - line +1;  // Remove mxtime
    if (mxtime > 0) {
      uint32_t current_hash = GetHash(SettingsText(SET_SYSLOG_HOST), strlen(SettingsText(SET_SYSLOG_HOST)));
      if (syslog_host_hash != current_hash) {
        IPAddress temp_syslog_host_addr;
        if (!WifiHostByName(SettingsText(SET_SYSLOG_HOST), temp_syslog_host_addr)) {  // If sleep enabled this might result in exception so try to do it once using hash
          TasmotaGlobal.syslog_level = 0;
          TasmotaGlobal.syslog_timer = SYSLOG_TIMER;
          AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_RETRY_IN " %d " D_UNIT_SECOND), SYSLOG_TIMER);
          return;
        }
        syslog_host_hash = current_hash;
        syslog_host_addr = temp_syslog_host_addr;
      }
      if (!PortUdp.beginPacket(syslog_host_addr, Settings->syslog_port)) {
        TasmotaGlobal.syslog_level = 0;
        TasmotaGlobal.syslog_timer = SYSLOG_TIMER;
        AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_APPLICATION D_SYSLOG_HOST_NOT_FOUND ". " D_RETRY_IN " %d " D_UNIT_SECOND), SYSLOG_TIMER);
        return;
      }

      char header[64];
      snprintf_P(header, sizeof(header), PSTR("%s ESP-"), NetworkHostname());
      char* line_start = line +mxtime;
#ifdef ESP8266
      // Packets over 1460 bytes are not send
      uint32_t line_len;
      int32_t log_len = len -mxtime -1;
      while (log_len > 0) {
        PortUdp.write(header);
        line_len = (log_len > 1460) ? 1460 : log_len;
        PortUdp.write((uint8_t*)line_start, line_len);
        PortUdp.endPacket();
        log_len -= 1460;
        line_start += 1460;
      }
#else
      PortUdp.write((const uint8_t*)header, strlen(header));
      PortUdp.write((uint8_t*)line_start, len -mxtime -1);
      PortUdp.endPacket();
#endif

      delay(1);                          // Add time for UDP handling (#5512)
    }
  }
}

bool NeedLogRefresh(uint32_t req_loglevel, uint32_t index) {
  if (!TasmotaGlobal.log_buffer) { return false; }  // Leave now if there is no buffer available

#ifdef ESP32
  // this takes the mutex, and will be release when the class is destroyed -
  // i.e. when the functon leaves  You CAN call mutex.give() to leave early.
  TasAutoMutex mutex((SemaphoreHandle_t *)&TasmotaGlobal.log_buffer_mutex);
#endif  // ESP32

  // Skip initial buffer fill
  if (strlen(TasmotaGlobal.log_buffer) < LOG_BUFFER_SIZE / 2) { return false; }

  char* line;
  size_t len;
  if (!GetLog(req_loglevel, &index, &line, &len)) { return false; }
  return ((line - TasmotaGlobal.log_buffer) < LOG_BUFFER_SIZE / 4);
}

bool GetLog(uint32_t req_loglevel, uint32_t* index_p, char** entry_pp, size_t* len_p) {
  if (!TasmotaGlobal.log_buffer) { return false; }  // Leave now if there is no buffer available
  if (TasmotaGlobal.uptime < 3) { return false; }   // Allow time to setup correct log level

  uint32_t index = *index_p;
  if (!req_loglevel || (index == TasmotaGlobal.log_buffer_pointer)) { return false; }

#ifdef ESP32
  // this takes the mutex, and will be release when the class is destroyed -
  // i.e. when the functon leaves  You CAN call mutex.give() to leave early.
  TasAutoMutex mutex((SemaphoreHandle_t *)&TasmotaGlobal.log_buffer_mutex);
#endif  // ESP32

  if (!index) {                            // Dump all
    index = TasmotaGlobal.log_buffer[0];
  }

  do {
    size_t len = 0;
    uint32_t loglevel = 0;
    char* entry_p = TasmotaGlobal.log_buffer;
    do {
      uint32_t cur_idx = *entry_p;
      entry_p++;
      size_t tmp = strchrspn(entry_p, '\1');
      tmp++;                               // Skip terminating '\1'
      if (cur_idx == index) {              // Found the requested entry
        loglevel = *entry_p - '0';
        entry_p++;                         // Skip loglevel
        len = tmp -1;
        break;
      }
      entry_p += tmp;
    } while (entry_p < TasmotaGlobal.log_buffer + LOG_BUFFER_SIZE && *entry_p != '\0');
    index++;
    if (index > 255) { index = 1; }        // Skip 0 as it is not allowed
    *index_p = index;
    if ((len > 0) &&
        (loglevel <= req_loglevel) &&
        (TasmotaGlobal.masterlog_level <= req_loglevel)) {
      *entry_pp = entry_p;
      *len_p = len;
      return true;
    }
    delay(0);
  } while (index != TasmotaGlobal.log_buffer_pointer);
  return false;
}

void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_payload = nullptr, const char* log_data_retained = nullptr) {
  if (!TasmotaGlobal.enable_logging) { return; }
  // Store log_data in buffer
  // To lower heap usage log_data_payload may contain the payload data from MqttPublishPayload()
  //  and log_data_retained may contain optional retained message from MqttPublishPayload()
#ifdef ESP32
  // this takes the mutex, and will be release when the class is destroyed -
  // i.e. when the functon leaves  You CAN call mutex.give() to leave early.
  TasAutoMutex mutex((SemaphoreHandle_t *)&TasmotaGlobal.log_buffer_mutex);
#endif  // ESP32

  char mxtime[21];  // "13:45:21.999-123/12 "
  snprintf_P(mxtime, sizeof(mxtime), PSTR("%02d" D_HOUR_MINUTE_SEPARATOR "%02d" D_MINUTE_SECOND_SEPARATOR "%02d.%03d"),
    RtcTime.hour, RtcTime.minute, RtcTime.second, RtcMillis());
  if (Settings->flag5.show_heap_with_timestamp) {
#ifdef ESP8266
    snprintf_P(mxtime, sizeof(mxtime), PSTR("%s-%03d"),
      mxtime, ESP_getFreeHeap1024());
#else
    snprintf_P(mxtime, sizeof(mxtime), PSTR("%s-%03d/%02d"),
      mxtime, ESP_getFreeHeap1024(), ESP_getHeapFragmentation());
#endif
  }
  strcat(mxtime, " ");

  char empty[2] = { 0 };
  if (!log_data_payload) { log_data_payload = empty; }
  if (!log_data_retained) { log_data_retained = empty; }

  if ((loglevel <= TasmotaGlobal.seriallog_level) &&
      (TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) {
    TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
#ifdef USE_SERIAL_BRIDGE
    SerialBridgePrintf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
#endif  // USE_SERIAL_BRIDGE
  }

  if (!TasmotaGlobal.log_buffer) { return; }  // Leave now if there is no buffer available

  uint32_t highest_loglevel = Settings->weblog_level;
  if (Settings->mqttlog_level > highest_loglevel) { highest_loglevel = Settings->mqttlog_level; }
  if (TasmotaGlobal.syslog_level > highest_loglevel) { highest_loglevel = TasmotaGlobal.syslog_level; }
  if (TasmotaGlobal.templog_level > highest_loglevel) { highest_loglevel = TasmotaGlobal.templog_level; }
  if (TasmotaGlobal.uptime < 3) { highest_loglevel = LOG_LEVEL_DEBUG_MORE; }  // Log all before setup correct log level

  if ((loglevel <= highest_loglevel) &&    // Log only when needed
      (TasmotaGlobal.masterlog_level <= highest_loglevel)) {
    // Delimited, zero-terminated buffer of log lines.
    // Each entry has this format: [index][loglevel][log data]['\1']

    // Truncate log messages longer than MAX_LOGSZ which is the log buffer size minus 64 spare
    uint32_t log_data_len = strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained);
    char too_long[TOPSZ];
    if (log_data_len > MAX_LOGSZ) {
      snprintf_P(too_long, sizeof(too_long) - 20, PSTR("%s%s"), log_data, log_data_payload);   // 20 = strlen("... 123456 truncated")
      snprintf_P(too_long, sizeof(too_long), PSTR("%s... %d truncated"), too_long, log_data_len);
      log_data = too_long;
      log_data_payload = empty;
      log_data_retained = empty;
    }

    TasmotaGlobal.log_buffer_pointer &= 0xFF;
    if (!TasmotaGlobal.log_buffer_pointer) {
      TasmotaGlobal.log_buffer_pointer++;  // Index 0 is not allowed as it is the end of char string
    }
    while (TasmotaGlobal.log_buffer_pointer == TasmotaGlobal.log_buffer[0] ||  // If log already holds the next index, remove it
           strlen(TasmotaGlobal.log_buffer) + strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained) + strlen(mxtime) + 4 > LOG_BUFFER_SIZE)  // 4 = log_buffer_pointer + '\1' + '\0'
    {
      char* it = TasmotaGlobal.log_buffer;
      it++;                                // Skip log_buffer_pointer
      it += strchrspn(it, '\1');           // Skip log line
      it++;                                // Skip delimiting "\1"
      memmove(TasmotaGlobal.log_buffer, it, LOG_BUFFER_SIZE -(it-TasmotaGlobal.log_buffer));  // Move buffer forward to remove oldest log line
    }
    snprintf_P(TasmotaGlobal.log_buffer, LOG_BUFFER_SIZE, PSTR("%s%c%c%s%s%s%s\1"),
      TasmotaGlobal.log_buffer, TasmotaGlobal.log_buffer_pointer++, '0'+loglevel, mxtime, log_data, log_data_payload, log_data_retained);
    TasmotaGlobal.log_buffer_pointer &= 0xFF;
    if (!TasmotaGlobal.log_buffer_pointer) {
      TasmotaGlobal.log_buffer_pointer++;  // Index 0 is not allowed as it is the end of char string
    }
  }
}

void AddLog(uint32_t loglevel, PGM_P formatP, ...) {
  uint32_t highest_loglevel = TasmotaGlobal.seriallog_level;
  if (Settings->weblog_level > highest_loglevel) { highest_loglevel = Settings->weblog_level; }
  if (Settings->mqttlog_level > highest_loglevel) { highest_loglevel = Settings->mqttlog_level; }
  if (TasmotaGlobal.syslog_level > highest_loglevel) { highest_loglevel = TasmotaGlobal.syslog_level; }
  if (TasmotaGlobal.templog_level > highest_loglevel) { highest_loglevel = TasmotaGlobal.templog_level; }
  if (TasmotaGlobal.uptime < 3) { highest_loglevel = LOG_LEVEL_DEBUG_MORE; }  // Log all before setup correct log level

  // If no logging is requested then do not access heap to fight fragmentation
  if ((loglevel <= highest_loglevel) && (TasmotaGlobal.masterlog_level <= highest_loglevel)) {
    va_list arg;
    va_start(arg, formatP);
    char* log_data = ext_vsnprintf_malloc_P(formatP, arg);
    va_end(arg);
    if (log_data == nullptr) { return; }

    AddLogData(loglevel, log_data);
    free(log_data);
  }
}

void AddLogBuffer(uint32_t loglevel, uint8_t *buffer, uint32_t count)
{
  char hex_char[(count * 3) + 2];
  AddLog(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(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) {
  char log_data[4 + (count * size * 3)];

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

void AddLogSpi(bool hardware, uint32_t clk, uint32_t mosi, uint32_t miso) {
  // Needs optimization
  uint32_t enabled = (hardware) ? TasmotaGlobal.spi_enabled : TasmotaGlobal.soft_spi_enabled;
  switch(enabled) {
    case SPI_MOSI:
      AddLog(LOG_LEVEL_INFO, PSTR("SPI: %s using GPIO%02d(CLK) and GPIO%02d(MOSI)"),
        (hardware) ? PSTR("Hardware") : PSTR("Software"), clk, mosi);
      break;
    case SPI_MISO:
      AddLog(LOG_LEVEL_INFO, PSTR("SPI: %s using GPIO%02d(CLK) and GPIO%02d(MISO)"),
        (hardware) ? PSTR("Hardware") : PSTR("Software"), clk, miso);
      break;
    case SPI_MOSI_MISO:
      AddLog(LOG_LEVEL_INFO, PSTR("SPI: %s using GPIO%02d(CLK), GPIO%02d(MOSI) and GPIO%02d(MISO)"),
        (hardware) ? PSTR("Hardware") : PSTR("Software"), clk, mosi, miso);
      break;
  }
}

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

#ifdef USE_UNISHOX_COMPRESSION

#include <unishox.h>

Unishox compressor;

// New variant where you provide the String object yourself
int32_t DecompressNoAlloc(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 len;
}

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

#endif // USE_UNISHOX_COMPRESSION
