Tasmota/tasmota/tasmota_support/support.ino

2757 lines
87 KiB
C++
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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;
}
/*********************************************************************************************\
* 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.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 wont change
REASON_SOFT_WDT_RST = 3, // "Software Watchdog" software watch dog reset, GPIO status wont change
REASON_SOFT_RESTART = 4, // "Software/System restart" software restart ,system_restart , GPIO status wont 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;
}
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 == 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 = (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;
}
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);
}
/*********************************************************************************************\
* 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) {
/*
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));
}
// 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; 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
#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
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
\*********************************************************************************************/
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);
}
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 - <PRI>HOSTNAME TAG: MSG
<PRI> = 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 - <PRI>TIMESTAMP HOSTNAME TAG: MSG
<PRI> = 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 - <PRI>VERSION TIMESTAMP HOSTNAME APP_NAME PROCID STRUCTURED-DATA MSGID MSG
<PRI> = 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);
}
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;
}
}
/*********************************************************************************************\
* HTML and URL encode
\*********************************************************************************************/
const char kUnescapeCode[] = "&><\"\'\\";
const char kEscapeCode[] PROGMEM = "&amp;|&gt;|&lt;|&quot;|&apos;|&#92;";
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 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.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