From c3132594d35a195f8ece42c08b8bd28ce93683e2 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sat, 2 Apr 2022 11:43:29 +0200 Subject: [PATCH] Add support for improv as used by esp-web-tools --- BUILDS.md | 1 + CHANGELOG.md | 12 +- RELEASENOTES.md | 3 +- tasmota/my_user_config.h | 1 + tasmota/settings.h | 3 +- tasmota/support_features.ino | 4 +- tasmota/tasmota_configurations.h | 1 + tasmota/tasmota_version.h | 2 +- tasmota/xdrv_62_improv.ino | 383 +++++++++++++++++++++++++++++++ tools/decode-status.py | 6 +- 10 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 tasmota/xdrv_62_improv.ino diff --git a/BUILDS.md b/BUILDS.md index cbf2ecea3..733b991ec 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -9,6 +9,7 @@ Note: `minimal` variant is not listed as it shouldn't be used outside of the [up | Feature or Sensor | l | t | k | s | i | d | Remarks |-----------------------|---|-------|---|---|---|---|-------- | MY_LANGUAGE en_GB | x | x / x | x | x | x | x | +| USE_IMPROV | - | x / x | x | x | x | x | | USE_UFILESYS | - | - / x | - | - | - | - | | USE_ARDUINO_OTA | - | - / - | - | - | - | - | | USE_DOMOTICZ | - | x / x | x | x | x | - | diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bef73c0c..d2a312afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,17 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - Development -## [11.0.0.4] +## [11.0.0.5] +### Added +- Support for improv as used by esp-web-tools + +### Changed + + +### Fixed + + +## [11.0.0.4] 20220402 ### Added - Command ``RtcNtpserver 0/1`` to enable Tasmota NTP server when enabled by define ``RTC_NTP_SERVER`` - NeoPool JSON modules, power module, cell info, chlorine, conductivity and ionization diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1136eccf2..1c56d6c18 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -103,7 +103,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl http://ota.tasmo [Complete list](BUILDS.md) of available feature and sensors. -## Changelog v11.0.0.4 +## Changelog v11.0.0.5 ### Added - Command ``SetOption135 1`` to disable LVGL splash screen - Command ``SetOption136 1`` to disable single sensor reports from Tuya devices while keeping teleperiod reports [#15216](https://github.com/arendst/Tasmota/issues/15216) @@ -114,6 +114,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl http://ota.tasmo - NeoPool commands ``NPpHMin``, ``NPpHMax``, ``NPpH``, ``NPRedox``, ``NPHydrolysis``, ``NPIonization``, ``NPChlorine`` and ``NPControl`` [#15015](https://github.com/arendst/Tasmota/issues/15015) - NeoPool system voltages display - TasmotaSerial implement ``end()`` +- Support for improv as used by esp-web-tools - Support for up to four DS3502 digital potentiometers with command ``Wiper 0..127`` - Support for ADE7880 3 phase energy monitor as used in Shelly 3EM [#13515](https://github.com/arendst/Tasmota/issues/13515) - Support for PCF85363 RTC as used in Shelly 3EM [#13515](https://github.com/arendst/Tasmota/issues/13515) diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 25751644b..e80da076a 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -397,6 +397,7 @@ // -- Wifi Config tools --------------------------- #define WIFI_SOFT_AP_CHANNEL 1 // Soft Access Point Channel number between 1 and 13 as used by Wi-Fi Manager web GUI +#define USE_IMPROV // Add support for IMPROV serial protocol as used by esp-web-tools (+2k code) // -- ESP-NOW ------------------------------------- //#define USE_TASMESH // Enable Tasmota Mesh using ESP-NOW (+11k code) diff --git a/tasmota/settings.h b/tasmota/settings.h index fbfac2e42..795fd05b7 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -833,8 +833,9 @@ typedef struct { uint32_t baudrate; // 2CC uint32_t ultradeepsleep; // 2D0 uint16_t deepsleep_slip; // 2D4 + uint8_t improv_state; // 2D6 - uint8_t free_2d6[2]; // 2D6 + uint8_t free_2d7[1]; // 2D7 int32_t energy_kWhtoday_ph[3]; // 2D8 int32_t energy_kWhtotal_ph[3]; // 2E4 diff --git a/tasmota/support_features.ino b/tasmota/support_features.ino index 488ccd283..c2e6535c7 100644 --- a/tasmota/support_features.ino +++ b/tasmota/support_features.ino @@ -803,7 +803,9 @@ void ResponseAppendFeatures(void) #if defined(USE_I2C) && defined(USE_DS3502) feature8 |= 0x02000000; // xdrv_61_ds3502.ino #endif -// feature8 |= 0x04000000; +#ifdef USE_IMPROV + feature8 |= 0x04000000; // xdrv_62_improv.ino +#endif // feature8 |= 0x08000000; // feature8 |= 0x10000000; diff --git a/tasmota/tasmota_configurations.h b/tasmota/tasmota_configurations.h index 26821cdee..7da082738 100644 --- a/tasmota/tasmota_configurations.h +++ b/tasmota/tasmota_configurations.h @@ -782,6 +782,7 @@ #undef FIRMWARE_DISPLAYS // Disable tasmota-display with display drivers enabled #undef FIRMWARE_IR // Disable tasmota-ir with IR full protocols activated +#undef USE_IMPROV // Disable support for IMPROV serial protocol as used by esp-web-tools (+2k code) #undef USE_TASMESH // Disable Tasmota Mesh using ESP-NOW (+11k code) #undef USE_ARDUINO_OTA // Disable support for Arduino OTA #undef USE_INFLUXDB // Disable influxdb support (+5k code) diff --git a/tasmota/tasmota_version.h b/tasmota/tasmota_version.h index d9b1f0202..dbf3330d5 100644 --- a/tasmota/tasmota_version.h +++ b/tasmota/tasmota_version.h @@ -20,6 +20,6 @@ #ifndef _TASMOTA_VERSION_H_ #define _TASMOTA_VERSION_H_ -const uint32_t VERSION = 0x0B000004; // 11.0.0.4 +const uint32_t VERSION = 0x0B000005; // 11.0.0.5 #endif // _TASMOTA_VERSION_H_ diff --git a/tasmota/xdrv_62_improv.ino b/tasmota/xdrv_62_improv.ino new file mode 100644 index 000000000..32e86ba39 --- /dev/null +++ b/tasmota/xdrv_62_improv.ino @@ -0,0 +1,383 @@ +/* + xdrv_62_improv.ino - IMPROV support for Tasmota + + SPDX-FileCopyrightText: 2022 Theo Arends + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_IMPROV +/*********************************************************************************************\ + * Serial implementation of IMPROV for initial wifi configuration using esp-web-tools + * + * See https://esphome.github.io/esp-web-tools/ and https://www.improv-wifi.com/serial/ +\*********************************************************************************************/ + +#define XDRV_62 62 + +#define IMPROV_WIFI_TIMEOUT 30 // Max seconds wait for wifi connection after reconfig + +//#define IMPROV_DEBUG + +enum ImprovError { + IMPROV_ERROR_NONE = 0x00, + IMPROV_ERROR_INVALID_RPC = 0x01, + IMPROV_ERROR_UNKNOWN_RPC = 0x02, + IMPROV_ERROR_UNABLE_TO_CONNECT = 0x03, + IMPROV_ERROR_NOT_AUTHORIZED = 0x04, + IMPROV_ERROR_UNKNOWN = 0xFF, +}; + +enum ImprovState { + IMPROV_STATE_STOPPED = 0x00, + IMPROV_STATE_AWAITING_AUTHORIZATION = 0x01, + IMPROV_STATE_AUTHORIZED = 0x02, + IMPROV_STATE_PROVISIONING = 0x03, + IMPROV_STATE_PROVISIONED = 0x04, +}; + +enum ImprovCommand { + IMPROV_UNKNOWN = 0x00, + IMPROV_WIFI_SETTINGS = 0x01, + IMPROV_GET_CURRENT_STATE = 0x02, + IMPROV_GET_DEVICE_INFO = 0x03, + IMPROV_GET_WIFI_NETWORKS = 0x04, + IMPROV_BAD_CHECKSUM = 0xFF, +}; + +enum ImprovSerialType { + IMPROV_TYPE_CURRENT_STATE = 0x01, + IMPROV_TYPE_ERROR_STATE = 0x02, + IMPROV_TYPE_RPC = 0x03, + IMPROV_TYPE_RPC_RESPONSE = 0x04 +}; + +static const uint8_t IMPROV_SERIAL_VERSION = 1; + +struct IMPROV { + uint32_t last_read_byte; + uint8_t wifi_timeout; + uint8_t seriallog_level; + bool message; +} Improv; + +/*********************************************************************************************/ + +void ImprovWriteData(uint8_t* data, uint32_t size) { + data[0] = 'I'; + data[1] = 'M'; + data[2] = 'P'; + data[3] = 'R'; + data[4] = 'O'; + data[5] = 'V'; + data[6] = IMPROV_SERIAL_VERSION; // 0x01 + uint8_t checksum = 0x00; + for (uint32_t i = 0; i < size -1; i++) { + checksum += data[i]; + } + data[size -1] = checksum; + + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("IMP: Send '%*_H'"), size, data); + +// Serial.write(data, size); + for (uint32_t i = 0; i < size; i++) { + Serial.write(data[i]); + } + Serial.write('\n'); +} + +void ImprovSendCmndState(uint32_t command, uint32_t state) { + uint8_t data[11]; + data[7] = command; + data[8] = 1; + data[9] = state; + ImprovWriteData(data, sizeof(data)); +} + +void ImprovSendState(uint32_t state) { +#ifdef IMPROV_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: State %d"), state); +#endif + RtcSettings.improv_state = state; + ImprovSendCmndState(IMPROV_TYPE_CURRENT_STATE, state); // 0x01 +} + +void ImprovSendError(uint32_t error) { +#ifdef IMPROV_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: Error %d"), error); +#endif + ImprovSendCmndState(IMPROV_TYPE_ERROR_STATE, error); // 0x02 +} + +void ImprovSendResponse(uint8_t* response, uint32_t size) { + uint8_t data[9 + size]; + data[7] = IMPROV_TYPE_RPC_RESPONSE; // 0x04 + data[8] = size -1; + memcpy(data +9, response, size); + ImprovWriteData(data, sizeof(data)); +} + +void ImprovSendSetting(uint32_t command) { + char data[100]; + uint32_t len = 0; +#ifdef USE_WEBSERVER + len = ext_snprintf_P(data, sizeof(data), PSTR("01|http://%_I:%d|"), (uint32_t)WiFi.localIP(), WEB_PORT); + uint32_t str_pos = 2; + for (uint32_t i = 3; i < len; i++) { + if ('|' == data[i]) { + data[str_pos] = i - str_pos -1; + } + } + len -= 3; +#endif // USE_WEBSERVER + data[0] = command; + data[1] = len; + ImprovSendResponse((uint8_t*)data, len +3); +} + +bool ImprovParseSerialByte(void) { + // 0 1 2 3 4 5 6 7 8 9 8 + le +1 + // I M P R O V ve ty le data ... \n + // 49 4D 50 52 4F 56 01 xx yy ........ 0A + if (6 == TasmotaGlobal.serial_in_byte_counter) { + return (IMPROV_SERIAL_VERSION == TasmotaGlobal.serial_in_byte); + } + if (TasmotaGlobal.serial_in_byte_counter <= 8) { + return true; // Wait for type and length + } + uint32_t data_len = TasmotaGlobal.serial_in_buffer[8]; + if (TasmotaGlobal.serial_in_byte_counter <= 9 + data_len) { // Receive including '\n' + return true; // Wait for data + } + + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("IMP: Rcvd '%*_H'"), TasmotaGlobal.serial_in_byte_counter, TasmotaGlobal.serial_in_buffer); + + TasmotaGlobal.serial_in_byte_counter--; // Drop '\n' + uint8_t checksum = 0x00; + for (uint32_t i = 0; i < TasmotaGlobal.serial_in_byte_counter; i++) { + checksum += TasmotaGlobal.serial_in_buffer[i]; + } + if (checksum != TasmotaGlobal.serial_in_buffer[TasmotaGlobal.serial_in_byte_counter]) { + ImprovSendError(IMPROV_ERROR_INVALID_RPC); // 0x01 - CRC error + return false; + } + + uint32_t type = TasmotaGlobal.serial_in_buffer[7]; + if (IMPROV_TYPE_RPC == type) { // 0x03 + uint32_t data_length = TasmotaGlobal.serial_in_buffer[10]; + if (data_length != data_len - 2) { + return false; + } + + uint32_t command = TasmotaGlobal.serial_in_buffer[9]; + switch (command) { + case IMPROV_WIFI_SETTINGS: { // 0x01 +// if (RtcSettings.improv_state != IMPROV_STATE_AUTHORIZED) { +// ImprovSendError(IMPROV_ERROR_NOT_AUTHORIZED); // 0x04 +// } else { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 + // I M P R O V vs ty le co dl sl s s i d pl p a s s w o r d cr + uint32_t ssid_length = TasmotaGlobal.serial_in_buffer[11]; + uint32_t ssid_end = 12 + ssid_length; + uint32_t pass_length = TasmotaGlobal.serial_in_buffer[ssid_end]; + uint32_t pass_start = ssid_end + 1; + uint32_t pass_end = pass_start + pass_length; + TasmotaGlobal.serial_in_buffer[ssid_end] = '\0'; + char* ssid = &TasmotaGlobal.serial_in_buffer[12]; + TasmotaGlobal.serial_in_buffer[pass_end] = '\0'; + char* password = &TasmotaGlobal.serial_in_buffer[pass_start]; +#ifdef IMPROV_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: Ssid '%s', Password '%s'"), ssid, password); +#endif // IMPROV_DEBUG + Improv.wifi_timeout = IMPROV_WIFI_TIMEOUT; // Set WiFi connect timeout + ImprovSendState(IMPROV_STATE_PROVISIONING); + Settings->flag4.network_wifi = 1; // Enable WiFi + char cmnd[TOPSZ]; + snprintf_P(cmnd, sizeof(cmnd), PSTR(D_CMND_BACKLOG "0 " D_CMND_SSID "1 %s;" D_CMND_PASSWORD "1 %s"), ssid, password); + ExecuteCommand(cmnd, SRC_SERIAL); // Set SSID and Password and restart +// } + break; + } + case IMPROV_GET_CURRENT_STATE: { // 0x02 + ImprovSendState(RtcSettings.improv_state); + if (IMPROV_STATE_PROVISIONED == RtcSettings.improv_state) { + ImprovSendSetting(IMPROV_GET_CURRENT_STATE); + } + break; + } + case IMPROV_GET_DEVICE_INFO: { // 0x03 + char data[200]; + uint32_t len = snprintf_P(data, sizeof(data), PSTR("01|Tasmota|%s|%s|%s|"), + TasmotaGlobal.version, GetDeviceHardware().c_str(), SettingsText(SET_DEVICENAME)); + data[0] = IMPROV_GET_DEVICE_INFO; + data[1] = len -3; + + uint32_t str_pos = 2; + for (uint32_t i = 3; i < len; i++) { + if ('|' == data[i]) { + data[str_pos] = i - str_pos -1; + str_pos = i; + } + } + ImprovSendResponse((uint8_t*)data, len); + break; + } + case IMPROV_GET_WIFI_NETWORKS: { // 0x04 + char data[200]; + int n = WiFi.scanNetworks(); + if (n) { + // Sort networks + int indices[n]; + for (uint32_t i = 0; i < n; i++) { + indices[i] = i; + } + // RSSI SORT + for (uint32_t i = 0; i < n; i++) { + for (uint32_t j = i + 1; j < n; j++) { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { + std::swap(indices[i], indices[j]); + } + } + } + // Remove duplicates ( must be RSSI sorted ) + for (uint32_t i = 0; i < n; i++) { + if (-1 == indices[i]) { continue; } + String cssid = WiFi.SSID(indices[i]); + uint32_t cschn = WiFi.channel(indices[i]); + for (uint32_t j = i + 1; j < n; j++) { + if ((cssid == WiFi.SSID(indices[j])) && (cschn == WiFi.channel(indices[j]))) { + indices[j] = -1; // set dup aps to index -1 + } + } + } + + // Send networks + for (uint32_t i = 0; i < n; i++) { + if (-1 == indices[i]) { continue; } // Skip dups + int32_t rssi = WiFi.RSSI(indices[i]); + String ssid_copy = WiFi.SSID(indices[i]); + if (!ssid_copy.length()) { ssid_copy = F("no_name"); } + + // Send each ssid separately to avoid overflowing the buffer + uint32_t len = snprintf_P(data, sizeof(data), PSTR("01|%s|%d|%s|"), ssid_copy.c_str(), rssi, (ENC_TYPE_NONE == WiFi.encryptionType(indices[i]))?"NO":"YES"); + data[0] = IMPROV_GET_WIFI_NETWORKS; + data[1] = len -3; + + uint32_t str_pos = 2; + for (uint32_t i = 3; i < len; i++) { + if ('|' == data[i]) { + data[str_pos] = i - str_pos -1; + str_pos = i; + } + } + ImprovSendResponse((uint8_t*)data, len); + } + } + + // Send empty response to signify the end of the list. + data[0] = IMPROV_GET_WIFI_NETWORKS; + data[1] = 0; // Empty string + ImprovSendResponse((uint8_t*)data, 3); + break; + } +/* + case IMPROV_BAD_CHECKSUM: { // 0xFF + break; + } +*/ + default: + ImprovSendError(IMPROV_ERROR_UNKNOWN_RPC); // 0x02 - Unknown payload + } + } + + return false; +} + +/*********************************************************************************************/ + +bool ImprovSerialInput(void) { + // Check if received data is IMPROV data + if (6 == TasmotaGlobal.serial_in_byte_counter) { + TasmotaGlobal.serial_in_buffer[TasmotaGlobal.serial_in_byte_counter] = 0; + if (!strcmp_P(TasmotaGlobal.serial_in_buffer, PSTR("IMPROV"))) { + Improv.seriallog_level = TasmotaGlobal.seriallog_level; + TasmotaGlobal.seriallog_level = 0; // Disable seriallogging interfering with IMPROV + Improv.last_read_byte = millis(); + Improv.message = true; + } + } + if (Improv.message) { + uint32_t now = millis(); + if (now - Improv.last_read_byte < 50) { + TasmotaGlobal.serial_in_buffer[TasmotaGlobal.serial_in_byte_counter] = TasmotaGlobal.serial_in_byte; + if (ImprovParseSerialByte()) { + TasmotaGlobal.serial_in_byte_counter++; + TasmotaGlobal.serial_in_byte = 0; + Improv.last_read_byte = now; + return false; + } + } + Improv.message = false; + TasmotaGlobal.seriallog_level = Improv.seriallog_level; // Restore seriallogging + return true; + } + return false; +} + +void ImprovEverySecond(void) { + if (Improv.wifi_timeout) { + Improv.wifi_timeout--; + if (Improv.wifi_timeout < IMPROV_WIFI_TIMEOUT -3) { // Tasmota restarts after ssid or password change + if ((!TasmotaGlobal.global_state.wifi_down)) { + Improv.wifi_timeout = 0; + if (IMPROV_STATE_AUTHORIZED == RtcSettings.improv_state) { + RtcSettings.improv_state = IMPROV_STATE_PROVISIONED; + } + if (IMPROV_STATE_PROVISIONING == RtcSettings.improv_state) { + ImprovSendState(IMPROV_STATE_PROVISIONED); + ImprovSendSetting(IMPROV_WIFI_SETTINGS); + } + return; + } + } + if (!Improv.wifi_timeout) { + if (IMPROV_STATE_PROVISIONING == RtcSettings.improv_state) { + ImprovSendError(IMPROV_ERROR_UNABLE_TO_CONNECT); // 0x03 - WiFi connect timeout + ImprovSendState(IMPROV_STATE_AUTHORIZED); + } + } + } +} + +void ImprovInit(void) { + if (!RtcSettings.improv_state) { + RtcSettings.improv_state = IMPROV_STATE_AUTHORIZED; // Power on state (persistent during restarts) + } + Improv.wifi_timeout = IMPROV_WIFI_TIMEOUT; // Try to update state after restart +#ifdef IMPROV_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("IMP: State %d"), RtcSettings.improv_state); +#endif // IMPROV_DEBUG +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv62(uint8_t function) { + bool result = false; + + switch (function) { + case FUNC_EVERY_SECOND: + ImprovEverySecond(); + break; + case FUNC_SERIAL: + result = ImprovSerialInput(); + break; + case FUNC_PRE_INIT: + ImprovInit(); + break; + } + return result; +} + +#endif // USE_IMPROV diff --git a/tools/decode-status.py b/tools/decode-status.py index 5e08d08b8..e079eb5d4 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -191,7 +191,7 @@ a_setoption = [[ "(PWM) force PWM lights to start at same phase, default is to spread phases to minimze overlap (also needed for H-bridge)", "(Display & LVGL) force disabling default splash screen", "(TuyaSNS) When ON disable publish single SNS value on Tuya Receive (keep Teleperiod)", - "", + "(Tuya) When Set, avoid the (mqtt-) publish of Tuya MCU Heartbeat response if SetOption66 is active", "","","","", "","","","" ]] @@ -266,7 +266,7 @@ a_features = [[ "USE_HRG15","USE_VINDRIKTNING","USE_SCD40","USE_HM330X", "USE_HDC2010","USE_LSC_MCSL","USE_SONOFF_SPM","USE_SHIFT595", "USE_SDM230","USE_CM110x","USE_BL6523","USE_ADE7880", - "USE_PCF85363","USE_DS3502","","", + "USE_PCF85363","USE_DS3502","USE_IMPROV","", "","","","" ]] @@ -295,7 +295,7 @@ else: obj = json.load(fp) def StartDecode(): - print ("\n*** decode-status.py v11.0.0.4 by Theo Arends and Jacek Ziolkowski ***") + print ("\n*** decode-status.py v11.0.0.5 by Theo Arends and Jacek Ziolkowski ***") # print("Decoding\n{}".format(obj))