/* tasmota.ino - Tasmota firmware for iTead Sonoff, Wemos and NodeMCU hardware 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 . */ /*********************************************************************************************\ * Preferred IDE is Visual Studio Code with PlatformIO extension which doesn't need prerequisites * * Limited support for Arduino IDE needs Prerequisites: * - Change libraries/PubSubClient/src/PubSubClient.h * #define MQTT_MAX_PACKET_SIZE 1200 * * Arduino IDE 1.8.12 and up parameters for partly support * - Select IDE Tools - Board: "Generic ESP8266 Module" * - Select IDE Tools - Flash Mode: "DOUT (compatible)" * - Select IDE Tools - Flash Size: "1M (FS:none OTA:~502KB)" * - Select IDE Tools - LwIP Variant: "v2 Higher Bandwidth (no feature)" * - Select IDE Tools - VTables: "Flash" * - Select IDE Tools - Espressif FW: "nonos-sdk-2.2.1+100 (190703)" \*********************************************************************************************/ // Location specific includes #ifndef ESP32_STAGE // ESP32 Stage has no core_version.h file. Disable include via PlatformIO Option #include // Arduino_Esp8266 version information (ARDUINO_ESP8266_RELEASE and ARDUINO_ESP8266_RELEASE_2_7_1) #endif #include "tasmota_compat.h" #include "tasmota_version.h" // Tasmota version information #include "tasmota.h" // Enumeration used in my_user_config.h #include "my_user_config.h" // Fixed user configurable options #ifdef USE_TLS #include // We need to include before "tasmota_globals.h" to take precedence over the BearSSL version in Arduino #endif // USE_TLS #include "tasmota_globals.h" // Function prototypes and global configuration #include "i18n.h" // Language support configured by my_user_config.h #include "tasmota_template.h" // Hardware configuration // Libraries #include // Ota #include // Ota #include // Webserver, Updater #include #include #ifdef USE_ARDUINO_OTA #include // Arduino OTA #ifndef USE_DISCOVERY #define USE_DISCOVERY #endif #endif // USE_ARDUINO_OTA #ifdef USE_DISCOVERY #include // MQTT, Webserver, Arduino OTA #endif // USE_DISCOVERY //#ifdef USE_I2C #include // I2C support library //#endif // USE_I2C #ifdef USE_SPI #include // SPI support, TFT #endif // USE_SPI // Structs #include "settings.h" /*********************************************************************************************\ * Global variables \*********************************************************************************************/ WiFiUDP PortUdp; // UDP Syslog and Alexa struct { uint32_t global_update; // Timestamp of last global temperature and humidity update uint32_t baudrate; // Current Serial baudrate uint32_t pulse_timer[MAX_PULSETIMERS]; // Power off timer uint32_t blink_timer; // Power cycle timer uint32_t backlog_timer; // Timer for next command in backlog uint32_t loop_load_avg; // Indicative loop load average uint32_t log_buffer_pointer; // Index in log buffer uint32_t uptime; // Counting every second until 4294967295 = 130 year GpioOptionABits gpio_optiona; // GPIO Option_A flags void *log_buffer_mutex; // Control access to log buffer power_t power; // Current copy of Settings.power power_t rel_inverted; // Relay inverted flag (1 = (0 = On, 1 = Off)) power_t last_power; // Last power set state power_t blink_power; // Blink power state power_t blink_powersave; // Blink start power save state power_t blink_mask; // Blink relay active mask int serial_in_byte_counter; // Index in receive buffer float temperature_celsius; // Provide a global temperature to be used by some sensors float humidity; // Provide a global humidity to be used by some sensors float pressure_hpa; // Provide a global pressure to be used by some sensors uint16_t gpio_pin[MAX_GPIO_PIN]; // GPIO functions indexed by pin number myio my_module; // Active copy of Module GPIOs (17 x 16 bits) uint16_t blink_counter; // Number of blink cycles uint16_t seriallog_timer; // Timer to disable Seriallog uint16_t syslog_timer; // Timer to re-enable syslog_level uint16_t tele_period; // Tele period timer int16_t save_data_counter; // Counter and flag for config save to Flash RulesBitfield rules_flag; // Rule state flags (16 bits) bool serial_local; // Handle serial locally bool fallback_topic_flag; // Use Topic or FallbackTopic bool backlog_mutex; // Command backlog pending bool stop_flash_rotate; // Allow flash configuration rotation bool blinkstate; // LED state bool pwm_present; // Any PWM channel configured with SetOption15 0 bool i2c_enabled; // I2C configured bool ntp_force_sync; // Force NTP sync bool is_8285; // Hardware device ESP8266EX (0) or ESP8285 (1) bool skip_light_fade; // Temporarily skip light fading bool restart_halt; // Do not restart but stay in wait loop bool module_changed; // Indicate module changed since last restart StateBitfield global_state; // Global states (currently Wifi and Mqtt) (8 bits) uint8_t spi_enabled; // SPI configured uint8_t soft_spi_enabled; // Software SPI configured uint8_t blinks; // Number of LED blinks uint8_t restart_flag; // Tasmota restart flag uint8_t ota_state_flag; // OTA state flag uint8_t wifi_state_flag; // Wifi state flag uint8_t mqtt_cmnd_blocked; // Ignore flag for publish command uint8_t mqtt_cmnd_blocked_reset; // Count down to reset if needed uint8_t state_250mS; // State 250msecond per second flag uint8_t latching_relay_pulse; // Latching relay pulse timer uint8_t active_device; // Active device in ExecuteCommandPower uint8_t sleep; // Current copy of Settings.sleep uint8_t leds_present; // Max number of LED supported uint8_t led_inverted; // LED inverted flag (1 = (0 = On, 1 = Off)) uint8_t led_power; // LED power state uint8_t ledlnk_inverted; // Link LED inverted flag (1 = (0 = On, 1 = Off)) uint8_t pwm_inverted; // PWM inverted flag (1 = inverted) uint8_t energy_driver; // Energy monitor configured uint8_t light_driver; // Light module configured uint8_t light_type; // Light types uint8_t serial_in_byte; // Received byte uint8_t devices_present; // Max number of devices supported uint8_t masterlog_level; // Master log level used to override set log level uint8_t seriallog_level; // Current copy of Settings.seriallog_level uint8_t syslog_level; // Current copy of Settings.syslog_level uint8_t templog_level; // Temporary log level to be used by HTTP cm and Telegram uint8_t module_type; // Current copy of Settings.module or user template type uint8_t last_source; // Last command source uint8_t shutters_present; // Number of actual define shutters // uint8_t prepped_loglevel; // Delayed log level message #ifndef SUPPORT_IF_STATEMENT uint8_t backlog_index; // Command backlog index uint8_t backlog_pointer; // Command backlog pointer String backlog[MAX_BACKLOG]; // Command backlog buffer #endif char version[16]; // Composed version string like 255.255.255.255 char image_name[33]; // Code image and/or commit char hostname[33]; // Composed Wifi hostname char serial_in_buffer[INPUT_BUFFER_SIZE]; // Receive buffer char mqtt_client[99]; // Composed MQTT Clientname char mqtt_topic[TOPSZ]; // Composed MQTT topic char mqtt_data[MESSZ]; // MQTT publish buffer and web page ajax buffer char log_buffer[LOG_BUFFER_SIZE]; // Web log buffer } TasmotaGlobal; #ifdef SUPPORT_IF_STATEMENT #include LinkedList backlog; // Command backlog implemented with LinkedList #define BACKLOG_EMPTY (backlog.size() == 0) #else #define BACKLOG_EMPTY (TasmotaGlobal.backlog_pointer == TasmotaGlobal.backlog_index) #endif /*********************************************************************************************\ * Main \*********************************************************************************************/ void setup(void) { #ifdef ESP32 #ifdef DISABLE_ESP32_BROWNOUT DisableBrownout(); // Workaround possible weak LDO resulting in brownout detection during Wifi connection #endif #endif RtcPreInit(); memset(&TasmotaGlobal, 0, sizeof(TasmotaGlobal)); TasmotaGlobal.baudrate = APP_BAUDRATE; TasmotaGlobal.seriallog_timer = SERIALLOG_TIMER; TasmotaGlobal.temperature_celsius = NAN; TasmotaGlobal.blinks = 201; TasmotaGlobal.wifi_state_flag = WIFI_RESTART; TasmotaGlobal.tele_period = 9999; TasmotaGlobal.active_device = 1; TasmotaGlobal.global_state.data = 0xF; // Init global state (wifi_down, mqtt_down) to solve possible network issues RtcRebootLoad(); if (!RtcRebootValid()) { RtcReboot.fast_reboot_count = 0; } #ifdef FIRMWARE_MINIMAL RtcReboot.fast_reboot_count = 0; // Disable fast reboot and quick power cycle detection #else if (ResetReason() == REASON_DEEP_SLEEP_AWAKE) { RtcReboot.fast_reboot_count = 0; // Disable fast reboot and quick power cycle detection } else { RtcReboot.fast_reboot_count++; } #endif RtcRebootSave(); if (RtcSettingsLoad()) { uint32_t baudrate = (RtcSettings.baudrate / 300) * 300; // Make it a valid baudrate if (baudrate) { TasmotaGlobal.baudrate = baudrate; } } Serial.begin(TasmotaGlobal.baudrate); Serial.println(); // Serial.setRxBufferSize(INPUT_BUFFER_SIZE); // Default is 256 chars TasmotaGlobal.seriallog_level = LOG_LEVEL_INFO; // Allow specific serial messages until config loaded SettingsLoad(); SettingsDelta(); OsWatchInit(); TasmotaGlobal.seriallog_level = Settings.seriallog_level; TasmotaGlobal.syslog_level = Settings.syslog_level; TasmotaGlobal.module_changed = (Settings.module != Settings.last_module); if (TasmotaGlobal.module_changed) { Settings.baudrate = APP_BAUDRATE / 300; Settings.serial_config = TS_SERIAL_8N1; } SetSerialBaudrate(Settings.baudrate * 300); // Reset serial interface if current baudrate is different from requested baudrate if (1 == RtcReboot.fast_reboot_count) { // Allow setting override only when all is well UpdateQuickPowerCycle(true); } TasmotaGlobal.stop_flash_rotate = Settings.flag.stop_flash_rotate; // SetOption12 - Switch between dynamic or fixed slot flash save location TasmotaGlobal.save_data_counter = Settings.save_data; TasmotaGlobal.sleep = Settings.sleep; #ifndef USE_EMULATION Settings.flag2.emulation = 0; #else #ifndef USE_EMULATION_WEMO if (EMUL_WEMO == Settings.flag2.emulation) { Settings.flag2.emulation = 0; } #endif #ifndef USE_EMULATION_HUE if (EMUL_HUE == Settings.flag2.emulation) { Settings.flag2.emulation = 0; } #endif #endif // USE_EMULATION // AddLogBuffer(LOG_LEVEL_DEBUG, (uint8_t*)&TasmotaGlobal, sizeof(TasmotaGlobal)); if (Settings.param[P_BOOT_LOOP_OFFSET]) { // SetOption36 // Disable functionality as possible cause of fast restart within BOOT_LOOP_TIME seconds (Exception, WDT or restarts) if (RtcReboot.fast_reboot_count > Settings.param[P_BOOT_LOOP_OFFSET]) { // Restart twice Settings.flag3.user_esp8285_enable = 0; // SetOption51 - Enable ESP8285 user GPIO's - Disable ESP8285 Generic GPIOs interfering with flash SPI if (RtcReboot.fast_reboot_count > Settings.param[P_BOOT_LOOP_OFFSET] +1) { // Restart 3 times for (uint32_t i = 0; i < MAX_RULE_SETS; i++) { if (bitRead(Settings.rule_stop, i)) { bitWrite(Settings.rule_enabled, i, 0); // Disable rules causing boot loop } } } if (RtcReboot.fast_reboot_count > Settings.param[P_BOOT_LOOP_OFFSET] +2) { // Restarted 4 times Settings.rule_enabled = 0; // Disable all rules } if (RtcReboot.fast_reboot_count > Settings.param[P_BOOT_LOOP_OFFSET] +3) { // Restarted 5 times for (uint32_t i = 0; i < ARRAY_SIZE(Settings.my_gp.io); i++) { Settings.my_gp.io[i] = GPIO_NONE; // Reset user defined GPIO disabling sensors } } if (RtcReboot.fast_reboot_count > Settings.param[P_BOOT_LOOP_OFFSET] +4) { // Restarted 6 times Settings.module = Settings.fallback_module; // Reset module to fallback module // Settings.last_module = Settings.fallback_module; } AddLog_P(LOG_LEVEL_INFO, PSTR("FRC: " D_LOG_SOME_SETTINGS_RESET " (%d)"), RtcReboot.fast_reboot_count); } } snprintf_P(TasmotaGlobal.version, sizeof(TasmotaGlobal.version), PSTR("%d.%d.%d"), VERSION >> 24 & 0xff, VERSION >> 16 & 0xff, VERSION >> 8 & 0xff); // Release version 6.3.0 if (VERSION & 0xff) { // Development or patched version 6.3.0.10 snprintf_P(TasmotaGlobal.version, sizeof(TasmotaGlobal.version), PSTR("%s.%d"), TasmotaGlobal.version, VERSION & 0xff); } // Thehackbox inserts "release" or "commit number" before compiling using sed -i -e 's/PSTR("(%s)")/PSTR("(85cff52-%s)")/g' tasmota.ino snprintf_P(TasmotaGlobal.image_name, sizeof(TasmotaGlobal.image_name), PSTR("(%s)"), CODE_IMAGE_STR); // Results in (85cff52-tasmota) or (release-tasmota) Format(TasmotaGlobal.mqtt_client, SettingsText(SET_MQTT_CLIENT), sizeof(TasmotaGlobal.mqtt_client)); Format(TasmotaGlobal.mqtt_topic, SettingsText(SET_MQTT_TOPIC), sizeof(TasmotaGlobal.mqtt_topic)); if (strchr(SettingsText(SET_HOSTNAME), '%') != nullptr) { SettingsUpdateText(SET_HOSTNAME, WIFI_HOSTNAME); snprintf_P(TasmotaGlobal.hostname, sizeof(TasmotaGlobal.hostname)-1, SettingsText(SET_HOSTNAME), TasmotaGlobal.mqtt_topic, ESP_getChipId() & 0x1FFF); } else { snprintf_P(TasmotaGlobal.hostname, sizeof(TasmotaGlobal.hostname)-1, SettingsText(SET_HOSTNAME)); } GetEspHardwareType(); GpioInit(); WifiConnect(); SetPowerOnState(); AddLog_P(LOG_LEVEL_INFO, PSTR(D_PROJECT " %s %s " D_VERSION " %s%s-" ARDUINO_CORE_RELEASE "(%s)"), PROJECT, SettingsText(SET_DEVICENAME), TasmotaGlobal.version, TasmotaGlobal.image_name, GetBuildDateAndTime().c_str()); #ifdef FIRMWARE_MINIMAL AddLog_P(LOG_LEVEL_INFO, PSTR(D_WARNING_MINIMAL_VERSION)); #endif // FIRMWARE_MINIMAL RtcInit(); #ifdef USE_ARDUINO_OTA ArduinoOTAInit(); #endif // USE_ARDUINO_OTA XdrvCall(FUNC_INIT); XsnsCall(FUNC_INIT); #ifdef USE_SCRIPT if (bitRead(Settings.rule_enabled, 0)) Run_Scripter(">BS",3,0); #endif TasmotaGlobal.rules_flag.system_init = 1; } void BacklogLoop(void) { if (TimeReached(TasmotaGlobal.backlog_timer)) { if (!BACKLOG_EMPTY && !TasmotaGlobal.backlog_mutex) { TasmotaGlobal.backlog_mutex = true; bool nodelay = false; bool nodelay_detected = false; String cmd; do { #ifdef SUPPORT_IF_STATEMENT cmd = backlog.shift(); #else cmd = TasmotaGlobal.backlog[TasmotaGlobal.backlog_pointer]; TasmotaGlobal.backlog[TasmotaGlobal.backlog_pointer] = (const char*) nullptr; // Force deallocation of the String internal memory TasmotaGlobal.backlog_pointer++; if (TasmotaGlobal.backlog_pointer >= MAX_BACKLOG) { TasmotaGlobal.backlog_pointer = 0; } #endif nodelay_detected = !strncasecmp_P(cmd.c_str(), PSTR(D_CMND_NODELAY), strlen(D_CMND_NODELAY)); if (nodelay_detected) { nodelay = true; } } while (!BACKLOG_EMPTY && nodelay_detected); if (!nodelay_detected) { ExecuteCommand((char*)cmd.c_str(), SRC_BACKLOG); } if (nodelay) { TasmotaGlobal.backlog_timer = millis(); // Reset backlog_timer which has been set by ExecuteCommand (CommandHandler) } TasmotaGlobal.backlog_mutex = false; } } } void SleepDelay(uint32_t mseconds) { if (mseconds) { for (uint32_t wait = 0; wait < mseconds; wait++) { delay(1); if (Serial.available()) { break; } // We need to service serial buffer ASAP as otherwise we get uart buffer overrun } } else { delay(0); } } void loop(void) { uint32_t my_sleep = millis(); XdrvCall(FUNC_LOOP); XsnsCall(FUNC_LOOP); OsWatchLoop(); ButtonLoop(); SwitchLoop(); #ifdef USE_DEVICE_GROUPS DeviceGroupsLoop(); #endif // USE_DEVICE_GROUPS BacklogLoop(); static uint32_t state_50msecond = 0; // State 50msecond timer if (TimeReached(state_50msecond)) { SetNextTimeInterval(state_50msecond, 50); #ifdef ROTARY_V1 RotaryHandler(); #endif // ROTARY_V1 XdrvCall(FUNC_EVERY_50_MSECOND); XsnsCall(FUNC_EVERY_50_MSECOND); } static uint32_t state_100msecond = 0; // State 100msecond timer if (TimeReached(state_100msecond)) { SetNextTimeInterval(state_100msecond, 100); Every100mSeconds(); XdrvCall(FUNC_EVERY_100_MSECOND); XsnsCall(FUNC_EVERY_100_MSECOND); } static uint32_t state_250msecond = 0; // State 250msecond timer if (TimeReached(state_250msecond)) { SetNextTimeInterval(state_250msecond, 250); Every250mSeconds(); XdrvCall(FUNC_EVERY_250_MSECOND); XsnsCall(FUNC_EVERY_250_MSECOND); } static uint32_t state_second = 0; // State second timer if (TimeReached(state_second)) { SetNextTimeInterval(state_second, 1000); PerformEverySecond(); XdrvCall(FUNC_EVERY_SECOND); XsnsCall(FUNC_EVERY_SECOND); } if (!TasmotaGlobal.serial_local) { SerialInput(); } #ifdef USE_ARDUINO_OTA ArduinoOtaLoop(); #endif // USE_ARDUINO_OTA uint32_t my_activity = millis() - my_sleep; if (Settings.flag3.sleep_normal) { // SetOption60 - Enable normal sleep instead of dynamic sleep // yield(); // yield == delay(0), delay contains yield, auto yield in loop SleepDelay(TasmotaGlobal.sleep); // https://github.com/esp8266/Arduino/issues/2021 } else { if (my_activity < (uint32_t)TasmotaGlobal.sleep) { SleepDelay((uint32_t)TasmotaGlobal.sleep - my_activity); // Provide time for background tasks like wifi } else { if (TasmotaGlobal.global_state.network_down) { SleepDelay(my_activity /2); // If wifi down and my_activity > setoption36 then force loop delay to 1/3 of my_activity period } } } if (!my_activity) { my_activity++; } // We cannot divide by 0 uint32_t loop_delay = TasmotaGlobal.sleep; if (!loop_delay) { loop_delay++; } // We cannot divide by 0 uint32_t loops_per_second = 1000 / loop_delay; // We need to keep track of this many loops per second uint32_t this_cycle_ratio = 100 * my_activity / loop_delay; TasmotaGlobal.loop_load_avg = TasmotaGlobal.loop_load_avg - (TasmotaGlobal.loop_load_avg / loops_per_second) + (this_cycle_ratio / loops_per_second); // Take away one loop average away and add the new one }