/* 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 . */ extern "C" { extern struct rst_info resetInfo; } /*********************************************************************************************\ * ESP32 Watchdog \*********************************************************************************************/ #ifdef ESP32 // Watchdog - yield() resets the watchdog extern "C" void __yield(void); // original function from Arduino Core extern "C" void yield(void) { __yield(); feedLoopWDT(); } // patching delay(uint32_t ms) extern "C" void __real_delay(uint32_t ms); // original function from Arduino Core extern "C" void __wrap_delay(uint32_t ms) { #ifdef USE_ESP32_WDT if (ms) { feedLoopWDT(); } __real_delay(ms); feedLoopWDT(); #else __real_delay(ms); #endif } #endif // ESP32 /*********************************************************************************************\ * Watchdog extension (https://github.com/esp8266/Arduino/issues/1532) \*********************************************************************************************/ #ifdef ESP8266 #include 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(); } bool ResetReasonPowerOn(void) { uint32_t reset_reason = ESP_ResetInfoReason(); return ((reset_reason == REASON_DEFAULT_RST) || (reset_reason == REASON_EXT_SYS_RST)); } 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; // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: UnescapeIn %*_H"), *size, (uint8_t*)buffer); 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 // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: UnescapeOut %*_H"), *size, (uint8_t*)buffer); 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; } // remove spaces at the beginning and end of the string (but not in the middle) char* TrimSpace(char *p) { // Remove white-space character (' ','\t','\n','\v','\f','\r') char* write = p; char* read = p; char ch = '.'; // skip all leading spaces while (isspace(*read)) { read++; } // copy the rest do { ch = *read++; *write++ = ch; } while (ch != '\0'); // move to end read = p + strlen(p); // move backwards while (p != read) { read--; if (isspace(*read)) { *read = '\0'; } else { break; } } 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; } char* SetStr(const char* str) { if (nullptr == str) { str = PSTR(""); } // nullptr is considered empty string size_t str_len = strlen(str); if (0 == str_len) { return EmptyStr; } // return empty string char* new_str = (char*) malloc(str_len + 1); if (nullptr == new_str) { return EmptyStr; } // return empty string strlcpy(new_str, str, str_len + 1); return new_str; } char* 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); char *cp = strstr(case_source, case_search); if (cp) { uint32_t offset = cp - case_source; cp = (char*)source + offset; } return cp; } 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 == nullptr) { return p; } 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; } // Converts a Hex string (case insensitive) into an array of bytes // Returns the number of bytes in the array, or -1 if an error occured // The `out` buffer must be at least half the size of hex string int32_t HexToBytes(const char* hex, uint8_t* out, size_t out_len) { size_t len = strlen_P(hex); if (len % 2 != 0) { return -1; } size_t bytes_out = len / 2; if (bytes_out > out_len) { bytes_out = out_len; } for (size_t i = 0; i < bytes_out; i++) { char byte[3]; byte[0] = hex[i*2]; byte[1] = hex[i*2 + 1]; byte[2] = '\0'; char* endPtr; out[i] = strtoul(byte, &endPtr, 16); if (*endPtr != '\0') { return -1; } } return bytes_out; } 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(TASMOTA_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(TASMOTA_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(TASMOTA_VERSION)) { version <<= 8; i++; } // Now we should have a fully constructed version number in uint32_t form. return (version > TASMOTA_VERSION); } int32_t UpdateDevicesPresent(int32_t change) { int32_t difference = 0; int32_t devices_present = TasmotaGlobal.devices_present; // Between 0 and 32 devices_present += change; if (devices_present < 0) { // Support down to 0 difference = devices_present; devices_present = 0; } else if (devices_present >= POWER_SIZE) { // Support up to uint32_t as bitmask difference = devices_present - POWER_SIZE; devices_present = POWER_SIZE; } TasmotaGlobal.devices_present = devices_present; return difference; } 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 tc) { if (isnan(tc)) { return NAN; } float result = tc; if (Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit result = tc * 1.8f + 32; // Fahrenheit } result = result + (0.1f * Settings->temp_comp); return result; } float ConvertTempToCelsius(float tf) { if (isnan(tf)) { return NAN; } float result = tf; if (Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit result = (tf - 32) / 1.8f; // Celsius } return result; } void UpdateGlobalTemperature(float t) { if (!Settings->global_sensor_index[0] && !TasmotaGlobal.user_globals[0]) { TasmotaGlobal.global_update = TasmotaGlobal.uptime; TasmotaGlobal.temperature_celsius = t; } } float ConvertTemp(float t) { UpdateGlobalTemperature(t); return ConvertTempToFahrenheit(t); } 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; if (!Settings->global_sensor_index[1] && !TasmotaGlobal.user_globals[1]) { 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; } #ifdef USE_HEAT_INDEX float CalcTemHumToHeatIndex(float t, float h) { if (isnan(h) || isnan(t)) { return NAN; } if (!Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit t = t * 1.8f + 32; // Fahrenheit } float hi = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (h * 0.094)); if (hi > 79) { float pt = t * t; // pow(t, 2) float ph = h * h; // pow(h, 2) hi = -42.379 + 2.04901523 * t + 10.14333127 * h + -0.22475541 * t * h + -0.00683783 * pt + -0.05481717 * ph + 0.00122874 * pt * h + 0.00085282 * t * ph + -0.00000199 * pt * ph; if ((h < 13) && (t >= 80.0) && (t <= 112.0)) { hi -= ((13.0 - h) * 0.25) * sqrtf((17.0 - abs(t - 95.0)) * 0.05882); } else if ((h > 85.0) && (t >= 80.0) && (t <= 87.0)) { hi += ((h - 85.0) * 0.1) * ((87.0 - t) * 0.2); } } if (!Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit hi = (hi - 32) / 1.8f; // Celsius } return hi; } #endif // USE_HEAT_INDEX float CalcTempHumToAbsHum(float t, float h) { if (isnan(t) || isnan(h)) { return NAN; } // taken from https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ // precision is about 0.1°C in range -30 to 35°C // August-Roche-Magnus 6.1094 exp(17.625 x T)/(T + 243.04) // Buck (1981) 6.1121 exp(17.502 x T)/(T + 240.97) // reference https://www.eas.ualberta.ca/jdwilson/EAS372_13/Vomel_CIRES_satvpformulae.html if (Settings->flag.temperature_conversion) { // SetOption8 - Switch between Celsius or Fahrenheit t = (t - 32) / 1.8f; // Celsius } float temp = FastPrecisePow(2.718281828f, (17.67f * t) / (t + 243.5f)); const float mw = 18.01534f; // Molar mass of water g/mol const float r = 8.31447215f; // Universal gas constant J/mol/K // return (6.112 * temp * h * 2.1674) / (273.15 + t); // Simplified version return (6.112f * temp * h * mw) / ((273.15f + t) * r); // Long version } float ConvertHgToHpa(float p) { // Convert mmHg (or inHg) to hPa float result = 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 * 33.86389f; // inHg (double to float saves 16 bytes!) } else { result = p * 1.3332239f; // mmHg (double to float saves 16 bytes!) } } return result; } float ConvertPressure(float p) { // Convert hPa to mmHg (or inHg) float result = p; if (!Settings->global_sensor_index[2] && !TasmotaGlobal.user_globals[2]) { 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 "|DOWN|" D_CLOSE "|" // 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 "UP|" D_OPEN "|" // 100 "ALL" ; // 255 const uint8_t sNumbers[] PROGMEM = { 0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 2,2,2, 3,3, 4,4, 100,100, 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 = (iweb_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; } void AllowInterrupts(bool state) { if (!state) { // Stop interrupts XdrvXsnsCall(FUNC_INTERRUPT_STOP); #ifdef USE_EMULATION UdpDisconnect(); #endif // USE_EMULATION } else { // Start interrupts #ifdef USE_EMULATION UdpConnect(); #endif // USE_EMULATION XdrvXsnsCall(FUNC_INTERRUPT_START); } } /*********************************************************************************************\ * 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) { return (char*)TasmotaGlobal.mqtt_data.c_str(); } uint32_t ResponseSize(void) { return MAX_LOGSZ; // Arbitratry max length satisfying full log entry } uint32_t ResponseLength(void) { return TasmotaGlobal.mqtt_data.length(); } void ResponseClear(void) { // Reset string length to zero TasmotaGlobal.mqtt_data = ""; // TasmotaGlobal.mqtt_data = (const char*) nullptr; // Doesn't work on ESP32 as strlen() (in MqttPublishPayload) will fail (for obvious reasons) } void ResponseJsonStart(void) { // Insert a JSON start bracket { TasmotaGlobal.mqtt_data.setCharAt(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 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(); } int ResponseTime_P(const char* format, ...) // Content send snprintf_P char data { // This uses char strings. Be aware of sending %% if % is needed 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(); } 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 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(); } 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); int len = 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); #ifdef USE_HEAT_INDEX float heatindex = CalcTemHumToHeatIndex(f_temperature, f_humidity); int len2 = ResponseAppend_P(PSTR(",\"" D_JSON_HEATINDEX "\":%*_f"), Settings->flag2.temperature_resolution, &heatindex); return len + len2; #endif // USE_HEAT_INDEX return len; } int ResponseJsonEnd(void) { return ResponseAppend_P(PSTR("}")); } int ResponseJsonEndEnd(void) { return ResponseAppend_P(PSTR("}}")); } bool ResponseContains_P(const char* needle) { /* return (strstr_P(TasmotaGlobal.mqtt_data.c_str(), needle) != nullptr); */ return (strstr_P(ResponseData(), needle) != nullptr); } bool GetNextSensor(void) { static uint32_t start_time = 0; static uint8_t sensor_set = 0; ResponseClear(); int tele_period_save = TasmotaGlobal.tele_period; TasmotaGlobal.tele_period = 2; // Do not allow HA updates during next function call while (!ResponseLength()) { if (0 == sensor_set) { if (TimeReached(start_time)) { SetNextTimeInterval(start_time, 1000); sensor_set++; // Minimal loop time is 1 second } break; } else if (1 == sensor_set) { if (!XsnsCallNextJsonAppend()) { // ,"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089} sensor_set++; // Looped break; } } else { if (!XdrvCallNextJsonAppend()) { // ,"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089} sensor_set = 0; // Looped break; } } } // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DBG: GetNextSensor %d, %d"), sensor_set, ResponseLength()); TasmotaGlobal.tele_period = tele_period_save; if (ResponseLength()) { ResponseJsonStart(); // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} ResponseJsonEnd(); // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DBG: GetNextSensor %d, '%s'"), sensor_set, ResponseData()); return true; } return false; } /*********************************************************************************************\ * 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) { 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) { return (StrCaseStr_P(SettingsText(SET_TEMPLATE_NAME), search) != nullptr); } 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)); } // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: GetInternalTemplate %*_H"), sizeof(mytmplt8285), (uint8_t *)&template8); // 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, %*_V"), option, size / 2, (uint8_t *)ptr); } #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 // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: TemplateGpiosIn %*_H"), sizeof(mycfgio), (uint8_t *)&src); // 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 */ #ifdef ESP8266 if (6 == i) { j = 9; } if (8 == i) { j = 12; } dest[j] = src[i]; j++; #endif // ESP8266 #ifdef ESP32 #if CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 dest[i] = src[i]; #elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 if (22 == i) { j = 33; } // skip 22-32 dest[j] = src[i]; j++; #else // ESP32 dest[Esp32TemplateToPhy[i]] = src[i]; #endif // ESP32C2/C3/C6 and S2/S3 #endif // ESP32 } // 11 85 00 85 85 00 00 00 00 00 00 00 15 38 85 00 00 81 // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: TemplateGpiosOut %*_H"), sizeof(myio), (uint8_t *)gp); } 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) { #ifdef ESP8266 return (((pin > 5) && (pin < 9)) || (11 == pin)); #endif // ESP8266 #ifdef ESP32 #if CONFIG_IDF_TARGET_ESP32C2 return (((pin > 10) && (pin < 12)) || ((pin > 13) && (pin < 18))); // ESP32C3 has GPIOs 11-17 reserved for Flash, with some boards GPIOs 12 13 are useable #elif CONFIG_IDF_TARGET_ESP32C3 return ((pin > 13) && (pin < 18)); // ESP32C3 has GPIOs 11-17 reserved for Flash, with some boards GPIOs 11 12 13 are useable #elif CONFIG_IDF_TARGET_ESP32C6 return ((pin == 24) || (pin == 25) || (pin == 27) || (pin == 29) || (pin == 30)); // ESP32C6 has GPIOs 24-30 reserved for Flash, with some boards GPIOs 26 28 are useable #elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 return (pin > 21) && (pin < 33); // ESP32S2 skip 22-32 #else return (pin >= 28) && (pin <= 31); // ESP32 skip 28-31 #endif // ESP32C2/C3/C6 and S2/S3 #endif // ESP32 } bool RedPin(uint32_t pin) { // Pin may be dangerous to change, display in RED in template console #ifdef ESP8266 return (9 == pin) || (10 == pin); #endif // ESP8266 #ifdef ESP32 #if CONFIG_IDF_TARGET_ESP32C2 return (12 == pin) || (13 == pin); // ESP32C2: GPIOs 12 13 are usually used for Flash (mode QIO/QOUT) #elif CONFIG_IDF_TARGET_ESP32C3 return (11 == pin) || (12 == pin) || (13 == pin); // ESP32C3: GPIOs 11 12 13 are usually used for Flash (mode QIO/QOUT) #elif CONFIG_IDF_TARGET_ESP32C6 return (26 == pin) || (28 == pin); // ESP32C6: GPIOs 26 28 are usually used for Flash (mode QIO/QOUT) #elif CONFIG_IDF_TARGET_ESP32S2 return false; // No red pin on ESP32S3 #elif CONFIG_IDF_TARGET_ESP32S3 return (33 <= pin) && (37 >= pin); // ESP32S3: GPIOs 33..37 are usually used for PSRAM #else // 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 #endif // ESP32C2/C3/C6 and S2/S3 #endif // ESP32 } uint32_t ValidPin(uint32_t pin, uint32_t gpio, uint8_t isTuya = false) { if (FlashPin(pin)) { return GPIO_NONE; // Disable flash pins GPIO6, GPIO7, GPIO8 and GPIO11 } #ifdef ESP8266 if (((WEMOS == Settings->module) || isTuya) && !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 %*_V"), sizeof(Settings->user_template) / 2, (uint8_t*)&Settings->user_template); return true; } void TemplateJson(void) { // AddLog(LOG_LEVEL_DEBUG, PSTR("TPL: Show %*_V"), sizeof(Settings->user_template) / 2, (uint8_t*)&Settings->user_template); 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; spserial_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 //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 // Since core 3.0.4 the returned baudrate could even be 115942 instead of 115200 !!! uint32_t margin = 300; uint32_t baudrate = Serial.baudRate(); if (baudrate > 10000) { margin = 2400; } return (baudrate / margin) * margin; // 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 #if ARDUINO_USB_MODE // 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_MODE #endif // ESP32 } void SetSerialInitBegin(void) { TasmotaGlobal.baudrate = Settings->baudrate * 300; if ((GetSerialBaudrate() != TasmotaGlobal.baudrate) || (TS_SERIAL_8N1 != Settings->serial_config)) { SetSerialBegin(); } } 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) { #ifdef ESP32 #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 #ifdef USE_USB_CDC_CONSOLE if (!tasconsole_serial) { return; // USB console does not use serial } #endif // USE_USB_CDC_CONSOLE #endif // ESP32C3/C6, S2 or S3 #endif // ESP32 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 \*********************************************************************************************/ /* // No need to use 64-bit inline uint64_t GetMicros64() { #ifdef ESP8266 return micros64(); #endif #ifdef ESP32 return esp_timer_get_time(); #endif } inline int64_t TimeDifference64(uint64_t prev, uint64_t next) { return ((int64_t) (next - prev)); } int64_t TimePassedSince64(const uint64_t& timestamp) { return TimeDifference64(timestamp, GetMicros64()); } bool TimeReached64(const uint64_t& timer) { return TimePassedSince64(timer) >= 0; } */ // Return the time difference as a signed value, taking into account the timers may overflow. // Returned timediff is between -24.9 days and +24.9 days. // Returned value is positive when "next" is after "prev" 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. // This is roll-over proof. return TimePassedSince(timer) >= 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(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); } void SystemBusyDelay(uint32_t busy) { /* TasmotaGlobal.busy_time = millis(); SetNextTimeInterval(TasmotaGlobal.busy_time, busy +1); if (!TasmotaGlobal.busy_time) { TasmotaGlobal.busy_time++; } */ TasmotaGlobal.busy_time = busy; } void SystemBusyDelayExecute(void) { if (TasmotaGlobal.busy_time) { /* // Calls to millis() interrupt RMT and defeats our goal if (!TimeReached(TasmotaGlobal.busy_time)) { delay(1); } */ delay(TasmotaGlobal.busy_time); TasmotaGlobal.busy_time = 0; } } /*********************************************************************************************\ * 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)) { // <--- mxtime ---> TAG: <---------------------- MSG ----------------------------> // 00:00:02.096-029 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("SLG: " 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("SLG: " D_SYSLOG_HOST_NOT_FOUND ". " D_RETRY_IN " %d " D_UNIT_SECOND), SYSLOG_TIMER); return; } char header[64]; /* Legacy format (until v13.3.0.1) - HOSTNAME TAG: MSG SYSLOG-MSG = wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Result = 2023-12-20T13:41:11.825749+01:00 wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 and below message in syslog if hostname starts with a "z" 2023-12-17T00:09:52.797782+01:00 domus8 rsyslogd: Uncompression of a message failed with return code -3 - enable debug logging if you need further information. Message ignored. [v8.2302.0] Notice in both cases the date and time is taken from the syslog server */ // snprintf_P(header, sizeof(header), PSTR("%s ESP-"), NetworkHostname()); /* Legacy format - HOSTNAME TAG: MSG = Facility 16 (= local use 0), Severity 6 (= informational) => 16 * 8 + 6 = <134> SYSLOG-MSG = <134>wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Result = 2023-12-21T11:31:50.378816+01:00 wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Notice in both cases the date and time is taken from the syslog server. Uncompression message is gone. */ snprintf_P(header, sizeof(header), PSTR("<134>%s ESP-"), NetworkHostname()); // SYSLOG-MSG = <134>wemos5 Tasmota HTP: Web server active on wemos5 with IP address 192.168.2.172 // Result = 2023-12-21T11:31:50.378816+01:00 wemos5 Tasmota HTP: Web server active on wemos5 with IP address 192.168.2.172 // snprintf_P(header, sizeof(header), PSTR("<134>%s Tasmota "), NetworkHostname()); /* RFC3164 - BSD syslog protocol - TIMESTAMP HOSTNAME TAG: MSG = Facility 16 (= local use 0), Severity 6 (= informational) => 16 * 8 + 6 = <134> TIMESTAMP = Mmm dd hh:mm:ss TAG: = ESP-HTP: SYSLOG-MSG = <134>Jan 1 00:00:02 wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Result = 2023-01-01T00:00:02+01:00 wemos5 ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Notice Year is taken from syslog server. Month, day and time is provided by Tasmota device. No milliseconds */ // snprintf_P(header, sizeof(header), PSTR("<134>%s %s ESP-"), GetSyslogDate(line).c_str(), NetworkHostname()); // SYSLOG-MSG = <134>Jan 1 00:00:02 wemos5 Tasmota HTP: Web server active on wemos5 with IP address 192.168.2.172 // Result = 2023-01-01T00:00:02+01:00 wemos5 Tasmota HTP: Web server active on wemos5 with IP address 192.168.2.172 // snprintf_P(header, sizeof(header), PSTR("<134>%s %s Tasmota "), GetSyslogDate(line).c_str(), NetworkHostname()); /* RFC5425 - Syslog protocol - VERSION TIMESTAMP HOSTNAME APP_NAME PROCID STRUCTURED-DATA MSGID MSG = Facility 16 (= local use 0), Severity 6 (= informational) => 16 * 8 + 6 = <134> VERSION = 1 TIMESTAMP = yyyy-mm-ddThh:mm:ss.nnnnnn-hh:mm (= local with timezone) APP_NAME = Tasmota PROCID = - STRUCTURED-DATA = - MSGID = ESP-HTP: SYSLOG-MSG = <134>1 1970-01-01T00:00:02.096000+01:00 wemos5 Tasmota - - ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Result = 1970-01-01T00:00:02.096000+00:00 wemos5 Tasmota ESP-HTP: Web server active on wemos5 with IP address 192.168.2.172 Notice date and time is provided by Tasmota device. */ // char line_time[mxtime]; // subStr(line_time, line, " ", 1); // 00:00:02.096-026 // subStr(line_time, line_time, "-", 1); // 00:00:02.096 // String systime = GetDate() + line_time + "000" + GetTimeZone(); // 1970-01-01T00:00:02.096000+01:00 // snprintf_P(header, sizeof(header), PSTR("<134>1 %s %s Tasmota - - ESP-"), systime.c_str(), NetworkHostname()); // SYSLOG-MSG = <134>1 1970-01-01T00:00:02.096000+01:00 wemos5 Tasmota - - HTP: Web server active on wemos5 with IP address 192.168.2.172 // Result = 1970-01-01T00:00:02.096000+00:00 wemos5 Tasmota HTP: Web server active on wemos5 with IP address 192.168.2.172 // snprintf_P(header, sizeof(header), PSTR("<134>1 %s %s Tasmota - - "), systime.c_str(), 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) { // Ignore any logging when maxlog_level = 0 OR logging for levels equal or lower than maxlog_level if (!TasmotaGlobal.maxlog_level || (loglevel > TasmotaGlobal.maxlog_level)) { 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 } } } uint32_t HighestLogLevel() { 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 return highest_loglevel; } void AddLog(uint32_t loglevel, PGM_P formatP, ...) { #ifdef ESP32 if (xPortInIsrContext()) { // When called from an ISR, you should not send out logs. // Allocating memory from within an ISR is a big no-no. // Also long-time blocking like sending logs (especially to a syslog server) // is also really not a good idea from an ISR call. return; } #endif uint32_t highest_loglevel = HighestLogLevel(); // 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() { AddLogBuffer(LOG_LEVEL_DEBUG, (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); } /*********************************************************************************************\ * HTML and URL encode \*********************************************************************************************/ const char kUnescapeCode[] = "&><\"\'\\"; const char kEscapeCode[] PROGMEM = "&|>|<|"|'|\"; String HtmlEscape(const String unescaped) { char escaped[10]; size_t ulen = unescaped.length(); String result; result.reserve(ulen); // pre-reserve the required space to avoid mutiple reallocations for (size_t i = 0; i < ulen; i++) { char c = unescaped[i]; char *p = strchr(kUnescapeCode, c); if (p != nullptr) { result += GetTextIndexed(escaped, sizeof(escaped), p - kUnescapeCode, kEscapeCode); } else { result += c; } } return result; } String SettingsTextEscaped(uint32_t index) { return HtmlEscape(SettingsText(index)); } String UrlEscape(const char *unescaped) { static const char *hex = "0123456789ABCDEF"; String result; result.reserve(strlen(unescaped)); while (*unescaped != '\0') { if (('a' <= *unescaped && *unescaped <= 'z') || ('A' <= *unescaped && *unescaped <= 'Z') || ('0' <= *unescaped && *unescaped <= '9') || *unescaped == '-' || *unescaped == '_' || *unescaped == '.' || *unescaped == '~') { result += *unescaped; } else { result += '%'; result += hex[*unescaped >> 4]; result += hex[*unescaped & 0xf]; } unescaped++; } return result; } /*********************************************************************************************\ * Uncompress static PROGMEM strings \*********************************************************************************************/ #ifdef USE_UNISHOX_COMPRESSION #include 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