diff --git a/code/espurna/alexa.cpp b/code/espurna/alexa.cpp index 073673e7..068fc799 100644 --- a/code/espurna/alexa.cpp +++ b/code/espurna/alexa.cpp @@ -85,7 +85,7 @@ bool enabled() { String hostname() { auto out = getSetting("alexaName", build::hostname()); if (!out.length()) { - out = getHostname(); + out = systemHostname(); } return out; diff --git a/code/espurna/board.cpp b/code/espurna/board.cpp deleted file mode 100644 index 2b9f08be..00000000 --- a/code/espurna/board.cpp +++ /dev/null @@ -1,479 +0,0 @@ -/* - -BOARD MODULE - -*/ - -#include "espurna.h" -#include "relay.h" -#include "rtcmem.h" -#include "sensor.h" -#include "utils.h" - -#include - -//-------------------------------------------------------------------------------- - -#include - -const String& getChipId() { - static String value; - if (!value.length()) { - char buffer[7]; - value.reserve(sizeof(buffer)); - snprintf_P(buffer, sizeof(buffer), PSTR("%06X"), ESP.getChipId()); - value = buffer; - } - return value; -} - -const String& getIdentifier() { - static String value; - if (!value.length()) { - value += getAppName(); - value += '-'; - value += getChipId(); - } - return value; -} - -// Full Chip ID (aka MAC) -// based on the [esptool.py](https://github.com/espressif/esptool) implementation -// - register addresses: https://github.com/espressif/esptool/blob/737825ba8d7aa696e4a9213cad932bceafb79f51/esptool.py#L1140-L1143 -// - chip id & mac: https://github.com/espressif/esptool/blob/737825ba8d7aa696e4a9213cad932bceafb79f51/esptool.py#L1235-L1254 - -const String& getFullChipId() { - static String out; - - if (!out.length()) { - uint32_t regs[3] { - READ_PERI_REG(0x3ff00050), - READ_PERI_REG(0x3ff00054), - READ_PERI_REG(0x3ff0005c)}; - - uint8_t mac[6] { - 0xff, - 0xff, - 0xff, - static_cast((regs[1] >> 8ul) & 0xfful), - static_cast(regs[1] & 0xffu), - static_cast((regs[0] >> 24ul) & 0xffu)}; - - if (mac[2] != 0) { - mac[0] = (regs[2] >> 16ul) & 0xffu; - mac[1] = (regs[2] >> 8ul) & 0xffu; - mac[2] = (regs[2] & 0xffu); - } else if (0 == ((regs[1] >> 16ul) & 0xff)) { - mac[0] = 0x18; - mac[1] = 0xfe; - mac[2] = 0x34; - } else if (1 == ((regs[1] >> 16ul) & 0xff)) { - mac[0] = 0xac; - mac[1] = 0xd0; - mac[2] = 0x74; - } - - char buffer[(sizeof(mac) * 2) + 1]; - if (hexEncode(mac, sizeof(mac), buffer, sizeof(buffer))) { - out = buffer; - } - } - - return out; -} - -const char* getEspurnaModules() { - static const char modules[] PROGMEM = -#if ALEXA_SUPPORT - "ALEXA " -#endif -#if API_SUPPORT - "API " -#endif -#if BUTTON_SUPPORT - "BUTTON " -#endif -#if DEBUG_SERIAL_SUPPORT - "DEBUG_SERIAL " -#endif -#if DEBUG_TELNET_SUPPORT - "DEBUG_TELNET " -#endif -#if DEBUG_UDP_SUPPORT - "DEBUG_UDP " -#endif -#if DEBUG_WEB_SUPPORT - "DEBUG_WEB " -#endif -#if DOMOTICZ_SUPPORT - "DOMOTICZ " -#endif -#if ENCODER_SUPPORT - "ENCODER " -#endif -#if FAN_SUPPORT - "FAN " -#endif -#if HOMEASSISTANT_SUPPORT - "HOMEASSISTANT " -#endif -#if I2C_SUPPORT - "I2C " -#endif -#if INFLUXDB_SUPPORT - "INFLUXDB " -#endif -#if IR_SUPPORT - "IR " -#endif -#if LED_SUPPORT - "LED " -#endif -#if LLMNR_SUPPORT - "LLMNR " -#endif -#if MDNS_SERVER_SUPPORT - "MDNS " -#endif -#if MQTT_SUPPORT - "MQTT " -#endif -#if NETBIOS_SUPPORT - "NETBIOS " -#endif -#if NOFUSS_SUPPORT - "NOFUSS " -#endif -#if NTP_SUPPORT - "NTP " -#endif -#if OTA_ARDUINOOTA_SUPPORT - "ARDUINO_OTA " -#endif -#if OTA_WEB_SUPPORT - "OTA_WEB " -#endif -#if (OTA_CLIENT != OTA_CLIENT_NONE) - "OTA_CLIENT " -#endif -#if PROMETHEUS_SUPPORT - "METRICS " -#endif -#if RELAY_SUPPORT - "RELAY " -#endif -#if RFM69_SUPPORT - "RFM69 " -#endif -#if RFB_SUPPORT - "RFB " -#endif -#if RPN_RULES_SUPPORT - "RPN_RULES " -#endif -#if SCHEDULER_SUPPORT - "SCHEDULER " -#endif -#if SENSOR_SUPPORT - "SENSOR " -#endif -#if SPIFFS_SUPPORT - "SPIFFS " -#endif -#if SSDP_SUPPORT - "SSDP " -#endif -#if TELNET_SUPPORT -#if TELNET_SERVER == TELNET_SERVER_WIFISERVER - "TELNET_SYNC " -#else - "TELNET " -#endif // TELNET_SERVER == TELNET_SERVER_WIFISERVER -#endif -#if TERMINAL_SUPPORT - "TERMINAL " -#endif -#if GARLAND_SUPPORT - "GARLAND " -#endif -#if THERMOSTAT_SUPPORT - "THERMOSTAT " -#endif -#if THERMOSTAT_DISPLAY_SUPPORT - "THERMOSTAT_DISPLAY " -#endif -#if THINGSPEAK_SUPPORT - "THINGSPEAK " -#endif -#if UART_MQTT_SUPPORT - "UART_MQTT " -#endif -#if WEB_SUPPORT -#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL - "WEB_SMALL " -#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT - "WEB_LIGHT " -#elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR - "WEB_SENSOR " -#elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE - "WEB_RFBRIDGE " -#elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69 - "WEB_RFM69 " -#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX - "WEB_LIGHTFOX " -#elif WEBUI_IMAGE == WEBUI_IMAGE_GARLAND - "WEB_GARLAND " -#elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT - "WEB_THERMOSTAT " -#elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN - "WEB_CURTAIN " -#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL - "WEB_FULL " -#endif -#endif - ""; - - return modules; -} - -#if SENSOR_SUPPORT - -const char* getEspurnaSensors() { - static const char sensors[] PROGMEM = -#if ADE7953_SUPPORT - "ADE7953 " -#endif -#if AM2320_SUPPORT - "AM2320_I2C " -#endif -#if ANALOG_SUPPORT - "ANALOG " -#endif -#if BH1750_SUPPORT - "BH1750 " -#endif -#if BMP180_SUPPORT - "BMP180 " -#endif -#if BMX280_SUPPORT - "BMX280 " -#endif -#if BME680_SUPPORT - "BME680 " -#endif -#if CSE7766_SUPPORT - "CSE7766 " -#endif -#if DALLAS_SUPPORT - "DALLAS " -#endif -#if DHT_SUPPORT - "DHTXX " -#endif -#if DIGITAL_SUPPORT - "DIGITAL " -#endif -#if ECH1560_SUPPORT - "ECH1560 " -#endif -#if EMON_ADC121_SUPPORT - "EMON_ADC121 " -#endif -#if EMON_ADS1X15_SUPPORT - "EMON_ADX1X15 " -#endif -#if EMON_ANALOG_SUPPORT - "EMON_ANALOG " -#endif -#if EVENTS_SUPPORT - "EVENTS " -#endif -#if GEIGER_SUPPORT - "GEIGER " -#endif -#if GUVAS12SD_SUPPORT - "GUVAS12SD " -#endif -#if HDC1080_SUPPORT - "HDC1080 " -#endif -#if HLW8012_SUPPORT - "HLW8012 " -#endif -#if INA219_SUPPORT - "INA219 " -#endif -#if LDR_SUPPORT - "LDR " -#endif -#if MAX6675_SUPPORT - "MAX6675 " -#endif -#if MHZ19_SUPPORT - "MHZ19 " -#endif -#if MICS2710_SUPPORT - "MICS2710 " -#endif -#if MICS5525_SUPPORT - "MICS5525 " -#endif -#if NTC_SUPPORT - "NTC " -#endif -#if PM1006_SUPPORT - "PM1006 " -#endif -#if PMSX003_SUPPORT - "PMSX003 " -#endif -#if PULSEMETER_SUPPORT - "PULSEMETER " -#endif -#if PZEM004T_SUPPORT - "PZEM004T " -#endif -#if PZEM004TV30_SUPPORT - "PZEM004TV30 " -#endif -#if SDS011_SUPPORT - "SDS011 " -#endif -#if SENSEAIR_SUPPORT - "SENSEAIR " -#endif -#if SHT3X_I2C_SUPPORT - "SHT3X_I2C " -#endif -#if SI7021_SUPPORT - "SI7021 " -#endif -#if SM300D2_SUPPORT - "SM300D2 " -#endif -#if SONAR_SUPPORT - "SONAR " -#endif -#if T6613_SUPPORT - "T6613 " -#endif -#if TMP3X_SUPPORT - "TMP3X " -#endif -#if V9261F_SUPPORT - "V9261F " -#endif -#if VEML6075_SUPPORT - "VEML6075 " -#endif -#if VL53L1X_SUPPORT - "VL53L1X " -#endif -#if EZOPH_SUPPORT - "EZOPH " -#endif -#if DUMMY_SENSOR_SUPPORT - "DUMMY " -#endif -#if SI1145_SUPPORT - "SI1145 " -#endif - ""; - - return sensors; -} - -#endif - -bool haveRelaysOrSensors() { - bool result = false; - result = (relayCount() > 0); -#if SENSOR_SUPPORT - result = result || (magnitudeCount() > 0); -#endif - return result; -} - -void boardSetup() { - // Some magic to allow seamless Tasmota OTA upgrades - // - inject dummy data sequence that is expected to hold current version info - // - purge settings, since we don't want accidentaly reading something as a kv - // - sometimes we cannot boot b/c of certain SDK params, purge last 16KiB - { - // ref. `SetOption78 1` in Tasmota - // - https://tasmota.github.io/docs/Commands/#setoptions (> SetOption78 Version check on Tasmota upgrade) - // - https://github.com/esphome/esphome/blob/0e59243b83913fc724d0229514a84b6ea14717cc/esphome/core/esphal.cpp#L275-L287 (the original idea from esphome) - // - https://github.com/arendst/Tasmota/blob/217addc2bb2cf46e7633c93e87954b245cb96556/tasmota/settings.ino#L218-L262 (specific checks, which succeed when finding 0xffffffff as version) - // - https://github.com/arendst/Tasmota/blob/0dfa38df89c8f2a1e582d53d79243881645be0b8/tasmota/i18n.h#L780-L782 (constants) - volatile uint32_t magic[] __attribute__((unused)) { - 0x5aa55aa5, - 0xffffffff, - 0xa55aa55a, - }; - - // ref. https://github.com/arendst/Tasmota/blob/217addc2bb2cf46e7633c93e87954b245cb96556/tasmota/settings.ino#L24 - // We will certainly find these when rebooting from Tasmota. Purge SDK as well, since we may experience WDT after starting up the softAP - auto* rtcmem = reinterpret_cast(RTCMEM_ADDR); - if ((0xA55A == rtcmem[64]) && (0xA55A == rtcmem[68])) { - DEBUG_MSG_P(PSTR("[BOARD] TASMOTA OTA, resetting...\n")); - rtcmem[64] = rtcmem[68] = 0; - customResetReason(CustomResetReason::Factory); - resetSettings(); - eraseSDKConfig(); - __builtin_trap(); - // can't return! - } - - // TODO: also check for things throughout the flash sector, somehow? - } - - // Workaround for SDK changes between 1.5.3 and 2.2.x or possible - // flash corruption happening to the 'default' WiFi config -#if SYSTEM_CHECK_ENABLED - if (!systemCheck()) { - const uint32_t Address { ESP.getFlashChipSize() - (FLASH_SECTOR_SIZE * 3) }; - - static constexpr size_t PageSize { 256 }; -#ifdef FLASH_PAGE_SIZE - static_assert(FLASH_PAGE_SIZE == PageSize, ""); -#endif - static constexpr auto Alignment = alignof(uint32_t); - alignas(Alignment) std::array page; - - if (ESP.flashRead(Address, reinterpret_cast(page.data()), page.size())) { - constexpr uint32_t ConfigOffset { 0xb0 }; - - // In case flash was already erased at some point, but we are still here - alignas(Alignment) const std::array Empty { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; - if (std::memcpy(&page[ConfigOffset], &Empty[0], Empty.size()) != 0) { - return; - } - - // 0x00B0: 0A 00 00 00 45 53 50 2D XX XX XX XX XX XX 00 00 ESP-XXXXXX - alignas(Alignment) const std::array Reference { 0xa0, 0x00, 0x00, 0x00, 0x45, 0x53, 0x50, 0x2d }; - if (std::memcmp(&page[ConfigOffset], &Reference[0], Reference.size()) != 0) { - DEBUG_MSG_P(PSTR("[BOARD] Invalid SDK config at 0x%08X, resetting...\n"), Address + ConfigOffset); - customResetReason(CustomResetReason::Factory); - systemForceStable(); - forceEraseSDKConfig(); - // can't return! - } - } - } -#endif - -#if DEBUG_SERIAL_SUPPORT - if (debugLogBuffer()) { - return; - } - - DEBUG_MSG_P(PSTR("[MAIN] %s %s built %s\n"), - getAppName(), getVersion(), buildTime().c_str()); - DEBUG_MSG_P(PSTR("[MAIN] %s\n"), getAppAuthor()); - DEBUG_MSG_P(PSTR("[MAIN] %s\n"), getAppWebsite()); - DEBUG_MSG_P(PSTR("[MAIN] Device: %s_%s\n"), - getManufacturer(), getDevice()); - DEBUG_MSG_P(PSTR("[MAIN] CPU chip ID: %s frequency: %hhuMHz\n"), - getFullChipId().c_str(), system_get_cpu_freq()); -#endif -} diff --git a/code/espurna/board.h b/code/espurna/board.h deleted file mode 100644 index 122722be..00000000 --- a/code/espurna/board.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - -BOARD MODULE - -*/ - -#pragma once - -#include - -const String& getChipId(); -const String& getFullChipId(); -const String& getIdentifier(); - -const char* getEspurnaModules(); -const char* getEspurnaSensors(); - -void boardSetup(); diff --git a/code/espurna/build.cpp b/code/espurna/build.cpp new file mode 100644 index 00000000..348f7660 --- /dev/null +++ b/code/espurna/build.cpp @@ -0,0 +1,356 @@ +/* + +BUILD INFO + +*/ + +#include "espurna.h" +#include "utils.h" + +#include + +//-------------------------------------------------------------------------------- + +namespace espurna { +namespace build { +namespace { + +namespace sdk { + +espurna::StringView base() { + // aka `const char SDK_VERSION[]` + return system_get_sdk_version(); +} + +espurna::StringView core_version() { + static const String out = ([]() { + String out; +#ifdef ARDUINO_ESP8266_RELEASE + out = ESP.getCoreVersion(); + if (out.equals("00000000")) { + out = String(ARDUINO_ESP8266_RELEASE); + } + out.replace('_', '.'); +#else +#define _GET_COREVERSION_STR(X) #X +#define GET_COREVERSION_STR(X) _GET_COREVERSION_STR(X) + out = GET_COREVERSION_STR(ARDUINO_ESP8266_GIT_DESC); +#undef _GET_COREVERSION_STR +#undef GET_COREVERSION_STR +#endif + return out; + })(); + + return out; +} + +espurna::StringView core_revision() { + static const String out = ([]() { +#ifdef ARDUINO_ESP8266_GIT_VER + return String(ARDUINO_ESP8266_GIT_VER, 16); +#else + return PSTR("(unspecified)"); +#endif + })(); + + return out; +} + +Sdk get() { + return Sdk{ + .base = base(), + .version = core_version(), + .revision = core_revision(), + }; +} + +} // namespace sdk + +namespace hardware { +namespace internal { + +alignas(4) static constexpr char Manufacturer[] PROGMEM = MANUFACTURER; +alignas(4) static constexpr char Device[] PROGMEM = DEVICE; + +} // namespace internal + +constexpr StringView manufacturer() { + return internal::Manufacturer; +} + +constexpr StringView device() { + return internal::Device; +} + +constexpr Hardware get() { + return Hardware{ + .manufacturer = manufacturer(), + .device = device(), + }; +} + +} // namespace device + +namespace app { +namespace internal { + +alignas(4) static constexpr char Modules[] PROGMEM = +#if ALEXA_SUPPORT + "ALEXA " +#endif +#if API_SUPPORT + "API " +#endif +#if BUTTON_SUPPORT + "BUTTON " +#endif +#if DEBUG_SERIAL_SUPPORT + "DEBUG_SERIAL " +#endif +#if DEBUG_TELNET_SUPPORT + "DEBUG_TELNET " +#endif +#if DEBUG_UDP_SUPPORT + "DEBUG_UDP " +#endif +#if DEBUG_WEB_SUPPORT + "DEBUG_WEB " +#endif +#if DOMOTICZ_SUPPORT + "DOMOTICZ " +#endif +#if ENCODER_SUPPORT + "ENCODER " +#endif +#if FAN_SUPPORT + "FAN " +#endif +#if HOMEASSISTANT_SUPPORT + "HOMEASSISTANT " +#endif +#if I2C_SUPPORT + "I2C " +#endif +#if INFLUXDB_SUPPORT + "INFLUXDB " +#endif +#if IR_SUPPORT + "IR " +#endif +#if LED_SUPPORT + "LED " +#endif +#if LLMNR_SUPPORT + "LLMNR " +#endif +#if MDNS_SERVER_SUPPORT + "MDNS " +#endif +#if MQTT_SUPPORT + "MQTT " +#endif +#if NETBIOS_SUPPORT + "NETBIOS " +#endif +#if NOFUSS_SUPPORT + "NOFUSS " +#endif +#if NTP_SUPPORT + "NTP " +#endif +#if OTA_ARDUINOOTA_SUPPORT + "ARDUINO_OTA " +#endif +#if OTA_WEB_SUPPORT + "OTA_WEB " +#endif +#if (OTA_CLIENT != OTA_CLIENT_NONE) + "OTA_CLIENT " +#endif +#if PROMETHEUS_SUPPORT + "METRICS " +#endif +#if RELAY_SUPPORT + "RELAY " +#endif +#if RFM69_SUPPORT + "RFM69 " +#endif +#if RFB_SUPPORT + "RFB " +#endif +#if RPN_RULES_SUPPORT + "RPN_RULES " +#endif +#if SCHEDULER_SUPPORT + "SCHEDULER " +#endif +#if SENSOR_SUPPORT + "SENSOR " +#endif +#if SPIFFS_SUPPORT + "SPIFFS " +#endif +#if SSDP_SUPPORT + "SSDP " +#endif +#if TELNET_SUPPORT +#if TELNET_SERVER == TELNET_SERVER_WIFISERVER + "TELNET_SYNC " +#else + "TELNET " +#endif // TELNET_SERVER == TELNET_SERVER_WIFISERVER +#endif +#if TERMINAL_SUPPORT + "TERMINAL " +#endif +#if GARLAND_SUPPORT + "GARLAND " +#endif +#if THERMOSTAT_SUPPORT + "THERMOSTAT " +#endif +#if THERMOSTAT_DISPLAY_SUPPORT + "THERMOSTAT_DISPLAY " +#endif +#if THINGSPEAK_SUPPORT + "THINGSPEAK " +#endif +#if UART_MQTT_SUPPORT + "UART_MQTT " +#endif +#if WEB_SUPPORT +#if WEBUI_IMAGE == WEBUI_IMAGE_SMALL + "WEB_SMALL " +#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHT + "WEB_LIGHT " +#elif WEBUI_IMAGE == WEBUI_IMAGE_SENSOR + "WEB_SENSOR " +#elif WEBUI_IMAGE == WEBUI_IMAGE_RFBRIDGE + "WEB_RFBRIDGE " +#elif WEBUI_IMAGE == WEBUI_IMAGE_RFM69 + "WEB_RFM69 " +#elif WEBUI_IMAGE == WEBUI_IMAGE_LIGHTFOX + "WEB_LIGHTFOX " +#elif WEBUI_IMAGE == WEBUI_IMAGE_GARLAND + "WEB_GARLAND " +#elif WEBUI_IMAGE == WEBUI_IMAGE_THERMOSTAT + "WEB_THERMOSTAT " +#elif WEBUI_IMAGE == WEBUI_IMAGE_CURTAIN + "WEB_CURTAIN " +#elif WEBUI_IMAGE == WEBUI_IMAGE_FULL + "WEB_FULL " +#endif +#endif + ""; + +alignas(4) static constexpr char Name[] PROGMEM = APP_NAME; +alignas(4) static constexpr char Version[] PROGMEM = APP_VERSION; +alignas(4) static constexpr char Author[] PROGMEM = APP_AUTHOR; +alignas(4) static constexpr char Website[] PROGMEM = APP_WEBSITE; + +} // namespace internal + +constexpr StringView modules() { + return internal::Modules; +} + +constexpr StringView name() { + return internal::Name; +} + +constexpr StringView version() { + return internal::Version; +} + +constexpr StringView author() { + return internal::Author; +} + +constexpr StringView website() { + return internal::Website; +} + +constexpr time_t time() { + return __UNIX_TIMESTAMP__; +} + +StringView time_string() { + static const String out = ([]() -> String { + char buf[32]; + +#if NTP_SUPPORT + const time_t ts = time(); + tm now; + + gmtime_r(&ts, &now); + now.tm_year += 1900; + now.tm_mon += 1; +#else + constexpr tm now { + .tm_year = __TIME_YEAR__, + .tm_mon = __TIME_MONTH__, + .tm_mday = __TIME_DAY__, + .tm_hour = __TIME_HOUR__, + .tm_min = __TIME_MINUTE__, + .tm_sec = __TIME_SECOND__, + }; +#endif + snprintf_P(buf, sizeof(buf), + PSTR("%04d-%02d-%02d %02d:%02d:%02d"), + now.tm_year, now.tm_mon, now.tm_mday, + now.tm_hour, now.tm_min, now.tm_sec); + + return buf; + })(); + + return out; +} + +App get() { + return App{ + .name = name(), + .version = version(), + .build_time = time_string(), + .author = author(), + .website = website(), + }; +}; + +} // namespace app + +Info info() { + return Info{ + .sdk = sdk::get(), + .hardware = hardware::get(), + .app = app::get(), + }; +} + +} // namespace +} // namespace build +} // namespace espurna + +time_t buildTime() { + return espurna::build::app::time(); +} + +espurna::build::Sdk buildSdk() { + return espurna::build::sdk::get(); +} + +espurna::build::Hardware buildHardware() { + return espurna::build::hardware::get(); +} + +espurna::build::App buildApp() { + return espurna::build::app::get(); +} + +espurna::build::Info buildInfo() { + return espurna::build::info(); +} + +espurna::StringView buildModules() { + return espurna::build::app::modules(); +} diff --git a/code/espurna/build.h b/code/espurna/build.h new file mode 100644 index 00000000..95479057 --- /dev/null +++ b/code/espurna/build.h @@ -0,0 +1,52 @@ +/* + +BUILD INFO + +*/ + +#pragma once + +#include + +#include "types.h" + +namespace espurna { +namespace build { + +struct Sdk { + StringView base; + StringView version; + StringView revision; +}; + +struct Hardware { + StringView manufacturer; + StringView device; +}; + +struct App { + StringView name; + StringView version; + StringView build_time; + StringView author; + StringView website; +}; + +struct Info { + Sdk sdk; + Hardware hardware; + App app; +}; + +} // namespace build +} // namespace espruna + +time_t buildTime(); + +espurna::build::Info buildInfo(); + +espurna::build::Sdk buildSdk(); +espurna::build::Hardware buildHardware(); +espurna::build::App buildApp(); + +espurna::StringView buildModules(); diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index e13f763a..64c96e99 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -9,10 +9,6 @@ // GENERAL //------------------------------------------------------------------------------ -#ifndef DEVICE_NAME -#define DEVICE_NAME MANUFACTURER "_" DEVICE // Concatenate both to get a unique device name -#endif - // When defined, ADMIN_PASS must be 8..63 printable ASCII characters. See: // https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution) // https://github.com/xoseperez/espurna/issues/1151 diff --git a/code/espurna/config/version.h b/code/espurna/config/version.h index fbf37cbc..f1c5fef0 100644 --- a/code/espurna/config/version.h +++ b/code/espurna/config/version.h @@ -21,5 +21,5 @@ #endif #ifndef CFG_VERSION -#define CFG_VERSION 11 +#define CFG_VERSION 12 #endif diff --git a/code/espurna/debug.cpp b/code/espurna/debug.cpp index f7e5db13..73eca916 100644 --- a/code/espurna/debug.cpp +++ b/code/espurna/debug.cpp @@ -14,7 +14,6 @@ Copyright (C) 2016-2019 by Xose Pérez #include "telnet.h" #include "web.h" #include "ntp.h" -#include "utils.h" #include "ws.h" #include @@ -476,7 +475,7 @@ void configure() { snprintf_P( internal::header, sizeof(internal::header), PSTR("<%u>1 - %.31s ESPurna - - - "), DEBUG_UDP_FAC_PRI, - getHostname().c_str()); + systemHostname().c_str()); } bool output(const char* message, size_t len) { @@ -552,7 +551,7 @@ void onVisible(JsonObject& root) { bool status(espurna::heartbeat::Mask mask) { if (mask & espurna::heartbeat::Report::Uptime) { - debugSend(PSTR("[MAIN] Uptime: %s\n"), getUptime().c_str()); + debugSend(PSTR("[MAIN] Uptime: %s\n"), prettyDuration(systemUptime()).c_str()); } if (mask & espurna::heartbeat::Report::Freeheap) { @@ -648,6 +647,29 @@ void debugWebSetup() { } #endif +void debugShowBanner() { +#if DEBUG_SERIAL_SUPPORT + if (espurna::debug::buffer::enabled()) { + return; + } + + const auto app = buildApp(); + DEBUG_MSG_P(PSTR("[MAIN] %s %s built %s\n"), + app.name.c_str(), app.version.c_str(), + app.build_time.c_str()); + + DEBUG_MSG_P(PSTR("[MAIN] %s\n"), app.author.c_str()); + DEBUG_MSG_P(PSTR("[MAIN] %s\n"), app.website.c_str()); + + DEBUG_MSG_P(PSTR("[MAIN] CPU chip ID: %s frequency: %hhuMHz\n"), + systemChipId().c_str(), system_get_cpu_freq()); + + const auto device = systemDevice(); + DEBUG_MSG_P(PSTR("[MAIN] Device: %s\n"), + device.c_str()); +#endif +} + void debugSetup() { #if DEBUG_SERIAL_SUPPORT DEBUG_PORT.begin(SERIAL_BAUDRATE); diff --git a/code/espurna/debug.h b/code/espurna/debug.h index a521f029..608f23cd 100644 --- a/code/espurna/debug.h +++ b/code/espurna/debug.h @@ -22,6 +22,7 @@ bool debugLogBuffer(); void debugWebSetup(); void debugConfigure(); void debugConfigureBoot(); +void debugShowBanner(); void debugSetup(); void debugSendRaw(const char* line, bool timestamp = false); diff --git a/code/espurna/domoticz.cpp b/code/espurna/domoticz.cpp index e81f89ca..bce1984e 100644 --- a/code/espurna/domoticz.cpp +++ b/code/espurna/domoticz.cpp @@ -437,7 +437,14 @@ bool onKeyCheck(espurna::StringView key, const JsonVariant&) { } void onVisible(JsonObject& root) { - if (haveRelaysOrSensors()) { + bool module { false }; +#if RELAY_SUPPORT + module = module || (relayCount() > 0); +#endif +#if SENSOR_SUPPORT + module = module || (magnitudeCount() > 0); +#endif + if (module) { wsPayloadModule(root, Prefix); } } diff --git a/code/espurna/espurna.h b/code/espurna/espurna.h index 2d623ab4..1883fb0d 100644 --- a/code/espurna/espurna.h +++ b/code/espurna/espurna.h @@ -26,8 +26,8 @@ along with this program. If not, see . #include "compat.h" +#include "build.h" #include "types.h" -#include "board.h" #include "debug.h" #include "gpio.h" #include "storage_eeprom.h" diff --git a/code/espurna/garland.cpp b/code/espurna/garland.cpp index 0c6c9b55..a31ae0f5 100644 --- a/code/espurna/garland.cpp +++ b/code/espurna/garland.cpp @@ -12,7 +12,7 @@ Currently animation calculation, brightness calculation/transition and showing m Debug output shows timings. Overal timing should be not more that 3000 ms. MQTT control: -Topic: DEVICE_NAME/garland/set +Topic: $root/garland/set Message: {"command":"string", "enable":"string", "brightness":int, "speed":int, "animation":"string", "palette":"string"/int, "duration":int} All parameters are optional. diff --git a/code/espurna/homeassistant.cpp b/code/espurna/homeassistant.cpp index e2c57d04..2469b10d 100644 --- a/code/espurna/homeassistant.cpp +++ b/code/espurna/homeassistant.cpp @@ -26,12 +26,16 @@ Copyright (C) 2019-2021 by Maxim Prokhorov #include +namespace espurna { namespace homeassistant { namespace { + namespace build { -const __FlashStringHelper* prefix() { - return F(HOMEASSISTANT_PREFIX); +alignas(4) static constexpr char Prefix[] PROGMEM = HOMEASSISTANT_PREFIX; + +constexpr StringView prefix() { + return Prefix; } constexpr bool enabled() { @@ -45,17 +49,24 @@ constexpr bool retain() { } // namespace build namespace settings { +namespace keys { + +alignas(4) static constexpr char Prefix[] PROGMEM = "haPrefix"; +alignas(4) static constexpr char Enabled[] PROGMEM = "haEnabled"; +alignas(4) static constexpr char Retain[] PROGMEM = "haRetain"; + +} // namespace keys String prefix() { - return getSetting("haPrefix", build::prefix()); + return getSetting(keys::Prefix, build::prefix()); } bool enabled() { - return getSetting("haEnabled", build::enabled()); + return getSetting(keys::Enabled, build::enabled()); } bool retain() { - return getSetting("haRetain", build::retain()); + return getSetting(keys::Retain, build::retain()); } } // namespace settings @@ -63,19 +74,17 @@ bool retain() { // Output is supposed to be used as both part of the MQTT config topic and the `uniq_id` field // TODO: manage UTF8 strings? in case we somehow receive `desc`, like it was done originally -String normalize_ascii(String input, bool lower) { - String output(std::move(input)); - - for (auto ptr = output.begin(); ptr != output.end(); ++ptr) { +String normalize_ascii(String value, bool lower) { + for (auto ptr = value.begin(); ptr != value.end(); ++ptr) { switch (*ptr) { case '\0': - goto return_output; + goto done; case '0' ... '9': case 'a' ... 'z': break; case 'A' ... 'Z': if (lower) { - *ptr += 32; + *ptr = (*ptr + 32); } break; default: @@ -84,50 +93,100 @@ String normalize_ascii(String input, bool lower) { } } -return_output: - return output; +done: + return value; +} + +String normalize_ascii(StringView value, bool lower) { + return normalize_ascii(String(value), lower); } // Common data used across the discovery payloads. // ref. https://developers.home-assistant.io/docs/entity_registry_index/ +// 'runtime' strings, may be changed in settings +struct ConfigStrings { + String name; + String identifier; + String prefix; +}; + +ConfigStrings make_config_strings() { + return ConfigStrings{ + .name = normalize_ascii(systemHostname(), false), + .identifier = normalize_ascii(systemIdentifier(), true), + .prefix = settings::prefix(), + }; +} + +// 'build-time' strings, always the same for current build +struct BuildStrings { + String version; + String manufacturer; + String device; +}; + +BuildStrings make_build_strings() { + BuildStrings out; + + const auto app = buildApp(); + out.version = String(app.version); + + const auto hardware = buildHardware(); + out.manufacturer = String(hardware.manufacturer); + out.device = String(hardware.device); + + return out; +} + class Device { public: - static constexpr size_t BufferSize { JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(5) }; + // XXX: take care when adding / removing keys and values below + // - `const char*` is copied by pointer value, persistent pointers make sure + // it is valid for the duration of this objects lifetime + // - `F(...)` aka `__FlashStringHelpe` **will take more space** + // it is **copied inside of the buffer**, and will take `strlen()` bytes + // - allocating more objects **will silently corrupt** buffer region + // while there are *some* checks, current version is going to break + static constexpr size_t BufferSize { JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(6) }; using Buffer = StaticJsonBuffer; using BufferPtr = std::unique_ptr; Device() = delete; + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; - Device(Device&&) = default; + Device(Device&&) = delete; + Device& operator=(Device&&) = delete; - template - Device(Args&&... args) : - _strings(make_strings(std::forward(args)...)), + Device(ConfigStrings config, BuildStrings build) : + _config(std::make_unique(std::move(config))), + _build(std::make_unique(std::move(build))), _buffer(std::make_unique()), _root(_buffer->createObject()) { - JsonArray& ids = _root.createNestedArray("ids"); - ids.add(_strings->identifier.c_str()); + _root["name"] = _config->name.c_str(); - _root["name"] = _strings->name.c_str(); - _root["sw"] = _strings->version; - _root["mf"] = _strings->manufacturer; - _root["mdl"] = _strings->device; + auto& ids = _root.createNestedArray("ids"); + ids.add(_config->identifier.c_str()); + + _root["sw"] = _build->version.c_str(); + _root["mf"] = _build->manufacturer.c_str(); + _root["mdl"] = _build->device.c_str(); } const String& name() const { - return _strings->name; + return _config->name; } const String& prefix() const { - return _strings->prefix; + return _config->prefix; } const String& identifier() const { - return _strings->identifier; + return _config->identifier; } JsonObject& root() { @@ -135,30 +194,12 @@ public: } private: - struct Strings { - String prefix; - String name; - String identifier; - const char* version; - const char* manufacturer; - const char* device; - }; + using ConfigStringsPtr = std::unique_ptr; + ConfigStringsPtr _config; - using StringsPtr = std::unique_ptr; + using BuildStringsPtr = std::unique_ptr; + BuildStringsPtr _build; - StringsPtr make_strings(String prefix, String name, String identifier, const char* version, const char* manufacturer, const char* device) { - return std::make_unique( - Strings{ - std::move(prefix), - normalize_ascii(std::move(name), false), - normalize_ascii(std::move(identifier), true), - version, - manufacturer, - device - }); - } - - StringsPtr _strings; BufferPtr _buffer; JsonObject& _root; }; @@ -169,7 +210,7 @@ using JsonBufferPtr = std::unique_ptr; class Context { public: Context() = delete; - Context(DevicePtr&& device, size_t capacity) : + Context(DevicePtr device, size_t capacity) : _device(std::move(device)), _capacity(capacity) {} @@ -222,19 +263,6 @@ private: size_t _capacity { 0ul }; }; -Context makeContext() { - auto device = std::make_unique( - settings::prefix(), - getHostname(), - getIdentifier(), - getVersion(), - getManufacturer(), - getDevice() - ); - - return Context(std::move(device), 2048ul); -} - String quote(String&& value) { if (value.equalsIgnoreCase("y") || value.equalsIgnoreCase("n") @@ -516,26 +544,26 @@ bool heartbeat(espurna::heartbeat::Mask mask) { JsonObject& root = buffer.createObject(); auto state = lightState(); - root["state"] = state ? "ON" : "OFF"; + root[F("state")] = state ? "ON" : "OFF"; if (state) { - root["brightness"] = lightBrightness(); + root[F("brightness")] = lightBrightness(); if (lightUseCCT()) { - root["white_value"] = lightColdWhite(); + root[F("white_value")] = lightColdWhite(); } if (lightColor()) { auto& color = root.createNestedObject("color"); if (lightUseRGB()) { auto rgb = lightRgb(); - color["r"] = rgb.red(); - color["g"] = rgb.green(); - color["b"] = rgb.blue(); + color[F("r")] = rgb.red(); + color[F("g")] = rgb.green(); + color[F("b")] = rgb.blue(); } else { auto hsv = lightHsv(); - color["h"] = hsv.hue(); - color["s"] = hsv.saturation(); + color[F("h")] = hsv.hue(); + color[F("s")] = hsv.saturation(); } } } @@ -551,7 +579,7 @@ bool heartbeat(espurna::heartbeat::Mask mask) { } void publishLightJson() { - heartbeat(static_cast(espurna::heartbeat::Report::Light)); + heartbeat(static_cast(heartbeat::Report::Light)); } void receiveLightJson(char* payload) { @@ -561,11 +589,11 @@ void receiveLightJson(char* payload) { return; } - if (!root.containsKey("state")) { + if (!root.containsKey(F("state"))) { return; } - auto state = root["state"].as(); + const auto state = root[F("state")].as(); if (state == F("ON")) { lightState(true); } else if (state == F("OFF")) { @@ -575,40 +603,40 @@ void receiveLightJson(char* payload) { } auto transition = lightTransitionTime(); - if (root.containsKey("transition")) { + if (root.containsKey(F("transition"))) { using LocalUnit = decltype(lightTransitionTime()); using RemoteUnit = std::chrono::duration; - auto seconds = RemoteUnit(root["transition"].as()); + auto seconds = RemoteUnit(root[F("transition")].as()); if (seconds.count() > 0.0f) { transition = std::chrono::duration_cast(seconds); } } - if (root.containsKey("color_temp")) { + if (root.containsKey(F("color_temp"))) { lightMireds(root["color_temp"].as()); } - if (root.containsKey("brightness")) { - lightBrightness(root["brightness"].as()); + if (root.containsKey(F("brightness"))) { + lightBrightness(root[F("brightness")].as()); } - if (lightHasColor() && root.containsKey("color")) { - JsonObject& color = root["color"]; + if (lightHasColor() && root.containsKey(F("color"))) { + JsonObject& color = root[F("color")]; if (lightUseRGB()) { lightRgb({ - color["r"].as(), - color["g"].as(), - color["b"].as()}); + color[F("r")].as(), + color[F("g")].as(), + color[F("b")].as()}); } else { lightHs( - color["h"].as(), - color["s"].as()); + color[F("h")].as(), + color[F("s")].as()); } } - if (lightUseCCT() && root.containsKey("white_value")) { - lightColdWhite(root["white_value"].as()); + if (lightUseCCT() && root.containsKey(F("white_value"))) { + lightColdWhite(root[F("white_value")].as()); } lightUpdate({transition, lightTransitionStep()}); @@ -716,9 +744,18 @@ private: #endif +DevicePtr make_device_ptr() { + return std::make_unique( + make_config_strings(), + make_build_strings()); +} + +Context make_context() { + return Context(make_device_ptr(), 2048); +} + // Reworked discovery class. Try to send and wait for MQTT QoS 1 publish ACK to continue. // Topic and message are generated on demand and most of JSON payload is cached for re-use to save RAM. - class DiscoveryTask { public: using Entity = std::unique_ptr; @@ -728,8 +765,17 @@ public: static constexpr espurna::duration::Milliseconds WaitLong { 1000 }; static constexpr int Retries { 5 }; - DiscoveryTask(bool enabled) : - _enabled(enabled) + DiscoveryTask() = delete; + + DiscoveryTask(const DiscoveryTask&) = delete; + DiscoveryTask& operator=(const DiscoveryTask&) = delete; + + DiscoveryTask(DiscoveryTask&&) = delete; + DiscoveryTask& operator=(DiscoveryTask&&) = delete; + + DiscoveryTask(Context ctx, bool enabled) : + _enabled(enabled), + _ctx(std::move(ctx)) {} void add(Entity&& entity) { @@ -798,10 +844,10 @@ public: private: bool _enabled { false }; - int _retry { Retries }; - Context _ctx { makeContext() }; + Entities _entities; + Context _ctx; }; constexpr espurna::duration::Milliseconds DiscoveryTask::WaitShort; @@ -910,7 +956,8 @@ void publishDiscovery() { return; } - auto task = std::make_shared(internal::enabled); + auto task = std::make_shared( + make_context(), internal::enabled); #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE task->add(); @@ -974,25 +1021,55 @@ namespace web { #if WEB_SUPPORT +alignas(4) static constexpr char Prefix[] PROGMEM = "ha"; + void onVisible(JsonObject& root) { - wsPayloadModule(root, PSTR("ha")); + wsPayloadModule(root, Prefix); } void onConnected(JsonObject& root) { - root["haPrefix"] = settings::prefix(); - root["haEnabled"] = settings::enabled(); - root["haRetain"] = settings::retain(); + root[FPSTR(settings::keys::Prefix)] = settings::prefix(); + root[FPSTR(settings::keys::Enabled)] = settings::enabled(); + root[FPSTR(settings::keys::Retain)] = settings::retain(); } bool onKeyCheck(espurna::StringView key, const JsonVariant& value) { - return espurna::settings::query::samePrefix(key, STRING_VIEW("ha")); + return espurna::settings::query::samePrefix(key, Prefix); } #endif } // namespace web + +void setup() { +#if WEB_SUPPORT + wsRegister() + .onVisible(web::onVisible) + .onConnected(web::onConnected) + .onKeyCheck(web::onKeyCheck); +#endif + +#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE + lightOnReport(publishLightJson); + mqttHeartbeat(heartbeat); +#endif + mqttRegister(mqttCallback); + +#if TERMINAL_SUPPORT + terminalRegisterCommand(F("HA.SEND"), [](::terminal::CommandContext&& ctx) { + internal::state = internal::State::Pending; + publishDiscovery(); + terminalOK(ctx); + }); +#endif + + espurnaRegisterReload(configure); + configure(); +} + } // namespace } // namespace homeassistant +} // namespace espurna // This module no longer implements .yaml generation, since we can't: // - use unique_id in the device config @@ -1001,29 +1078,7 @@ bool onKeyCheck(espurna::StringView key, const JsonVariant& value) { // (yet? needs reworked configuration section or making functions read settings directly) void haSetup() { -#if WEB_SUPPORT - wsRegister() - .onVisible(homeassistant::web::onVisible) - .onConnected(homeassistant::web::onConnected) - .onKeyCheck(homeassistant::web::onKeyCheck); -#endif - -#if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE - lightOnReport(homeassistant::publishLightJson); - mqttHeartbeat(homeassistant::heartbeat); -#endif - mqttRegister(homeassistant::mqttCallback); - -#if TERMINAL_SUPPORT - terminalRegisterCommand(F("HA.SEND"), [](::terminal::CommandContext&& ctx) { - homeassistant::internal::state = homeassistant::internal::State::Pending; - homeassistant::publishDiscovery(); - terminalOK(ctx); - }); -#endif - - espurnaRegisterReload(homeassistant::configure); - homeassistant::configure(); + espurna::homeassistant::setup(); } #endif // HOMEASSISTANT_SUPPORT diff --git a/code/espurna/influxdb.cpp b/code/espurna/influxdb.cpp index bc4881db..7ba7e7ce 100644 --- a/code/espurna/influxdb.cpp +++ b/code/espurna/influxdb.cpp @@ -216,7 +216,7 @@ void _idbFlush() { // TODO: should we always store specific pairs like tspk keeps relay / sensor readings? // note that we also send heartbeat data, persistent values should be flagged - const String device = getHostname(); + const String device = systemHostname(); _idb_client->payload = ""; for (auto& pair : _idb_client->values) { diff --git a/code/espurna/llmnr.cpp b/code/espurna/llmnr.cpp index cced26a4..7d84c3db 100644 --- a/code/espurna/llmnr.cpp +++ b/code/espurna/llmnr.cpp @@ -15,7 +15,7 @@ Copyright (C) 2017-2019 by Xose Pérez #include "llmnr.h" void llmnrSetup() { - auto hostname = getHostname(); + const auto hostname = systemHostname(); LLMNR.begin(hostname.c_str()); DEBUG_MSG_P(PSTR("[LLMNR] Configured for %s\n"), hostname.c_str()); } diff --git a/code/espurna/main.cpp b/code/espurna/main.cpp index 6802e5a3..ddd9a965 100644 --- a/code/espurna/main.cpp +++ b/code/espurna/main.cpp @@ -127,11 +127,6 @@ void setup() { terminalSetup(); #endif - // Hostname & board name initialization - setDefaultHostname(); - setBoardName(); - - boardSetup(); wifiSetup(); otaSetup(); @@ -139,6 +134,9 @@ void setup() { telnetSetup(); #endif + // Our app banner (usually, for uart) + debugShowBanner(); + // ------------------------------------------------------------------------- // Check if system is stable // ------------------------------------------------------------------------- diff --git a/code/espurna/mdns.cpp b/code/espurna/mdns.cpp index fb91cd9c..b8e3eb6a 100644 --- a/code/espurna/mdns.cpp +++ b/code/espurna/mdns.cpp @@ -36,12 +36,24 @@ void addServices() { #endif #if OTA_ARDUINOOTA_SUPPORT - if (MDNS.enableArduino(OTA_PORT, getAdminPass().length() > 0)) { - MDNS.addServiceTxt("arduino", "tcp", "app_name", getAppName()); - MDNS.addServiceTxt("arduino", "tcp", "app_version", getVersion()); - MDNS.addServiceTxt("arduino", "tcp", "build_date", buildTime()); - MDNS.addServiceTxt("arduino", "tcp", "mac", getFullChipId()); - MDNS.addServiceTxt("arduino", "tcp", "target_board", getBoardName()); + // MDNS implementation has its weird way of accessing strings; + // can't pass `String`s directly and must use a char pointer + // to RAM so it could copy it for internal use via `strcpy` + // Since all we use here is build data exposed with `StringView`s, + // force everything in RAM first to avoid a runtime exception. + if (MDNS.enableArduino(OTA_PORT, systemPassword().length() > 0)) { + const auto app = buildApp(); + MDNS.addServiceTxt("arduino", "tcp", + "app_name", String(app.name).c_str()); + MDNS.addServiceTxt("arduino", "tcp", + "app_version", String(app.version).c_str()); + MDNS.addServiceTxt("arduino", "tcp", + "build_date", String(app.build_time).c_str()); + + MDNS.addServiceTxt("arduino", "tcp", + "mac", String(systemChipId()).c_str()); + MDNS.addServiceTxt("arduino", "tcp", + "target_board", String(systemDevice()).c_str()); MDNS.addServiceTxt("arduino", "tcp", "mem_size", String(static_cast(ESP.getFlashChipRealSize() / 1024), 10)); @@ -54,7 +66,7 @@ void addServices() { } void start() { - auto hostname = getHostname(); + const auto hostname = systemHostname(); if (MDNS.begin(hostname)) { DEBUG_MSG_P(PSTR("[MDNS] Started with hostname %s\n"), hostname.c_str()); addServices(); diff --git a/code/espurna/migrate.cpp b/code/espurna/migrate.cpp index 95fb8be8..055236d8 100644 --- a/code/espurna/migrate.cpp +++ b/code/espurna/migrate.cpp @@ -8,6 +8,8 @@ Copyright (C) 2020-2021 by Maxim Prokhorov @@ -15,9 +17,10 @@ Copyright (C) 2020-2021 by Maxim Prokhorov to_purge; @@ -54,8 +55,35 @@ int currentVersion() { return 0; } -} // namespace +void run(MigrateVersionCallback callback) { + static const auto current = currentVersion(); + if (current) { + callback(current); + } +} + +void run() { + setSetting(FPSTR(schema::Key), schema::Version); + + if (currentVersion() < 4) { + delSetting(F("board")); + } + + if (currentVersion() < 12) { + const auto hostname = systemHostname(); + if (systemIdentifier() == hostname) { + delSetting(F("hostname")); + } + + delSetting(F("boardName")); + } + + saveSettings(); +} + } // namespace migrate + +} // namespace } // namespace settings } // namespace espurna @@ -68,23 +96,9 @@ int migrateVersion() { } void migrateVersion(MigrateVersionCallback callback) { - static const auto current = espurna::settings::migrate::currentVersion(); - if (current) { - callback(current); - } + return espurna::settings::migrate::run(callback); } void migrate() { - using namespace espurna::settings::schema; - setSetting(FPSTR(Key), Version); - - using namespace espurna::settings::migrate; - switch (currentVersion()) { - case 2: - case 3: - case 4: - delSetting(F("board")); - saveSettings(); - break; - } + espurna::settings::migrate::run(); } diff --git a/code/espurna/mqtt.cpp b/code/espurna/mqtt.cpp index 537ae3ab..611813c4 100644 --- a/code/espurna/mqtt.cpp +++ b/code/espurna/mqtt.cpp @@ -323,7 +323,7 @@ KeepAlive keepalive() { } String clientId() { - return getSetting(keys::ClientId, getIdentifier()); + return getSetting(keys::ClientId, systemIdentifier()); } String topicWill() { @@ -697,10 +697,13 @@ bool _mqttConnectSyncClient(bool secure = false) { #endif // (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) -String _mqttPlaceholders(String&& text) { - text.replace("{hostname}", getHostname()); - text.replace("{magnitude}", "#"); - text.replace("{mac}", getFullChipId()); +String _mqttPlaceholders(String text) { + static const String mac = String(systemChipId()); + text.replace(F("{mac}"), mac); + + text.replace(F("{hostname}"), systemHostname()); + text.replace(F("{magnitude}"), F("#")); + return text; } @@ -1033,22 +1036,23 @@ bool _mqttHeartbeat(espurna::heartbeat::Mask mask) { if (mask & espurna::heartbeat::Report::Interval) mqttSend(MQTT_TOPIC_INTERVAL, String(_mqtt_heartbeat_interval.count()).c_str()); + const auto app = buildApp(); if (mask & espurna::heartbeat::Report::App) - mqttSend(MQTT_TOPIC_APP, getAppName()); + mqttSend(MQTT_TOPIC_APP, String(app.name).c_str()); if (mask & espurna::heartbeat::Report::Version) - mqttSend(MQTT_TOPIC_VERSION, getVersion()); + mqttSend(MQTT_TOPIC_VERSION, String(app.version).c_str()); if (mask & espurna::heartbeat::Report::Board) - mqttSend(MQTT_TOPIC_BOARD, getBoardName().c_str()); + mqttSend(MQTT_TOPIC_BOARD, systemDevice().c_str()); if (mask & espurna::heartbeat::Report::Hostname) - mqttSend(MQTT_TOPIC_HOSTNAME, getHostname().c_str()); + mqttSend(MQTT_TOPIC_HOSTNAME, systemHostname().c_str()); if (mask & espurna::heartbeat::Report::Description) { - auto desc = getDescription(); - if (desc.length()) { - mqttSend(MQTT_TOPIC_DESCRIPTION, desc.c_str()); + const auto value = systemDescription(); + if (value.length()) { + mqttSend(MQTT_TOPIC_DESCRIPTION, value.c_str()); } } @@ -1076,7 +1080,7 @@ bool _mqttHeartbeat(espurna::heartbeat::Mask mask) { #endif if (mask & espurna::heartbeat::Report::Freeheap) { - auto stats = systemHeapStats(); + const auto stats = systemHeapStats(); mqttSend(MQTT_TOPIC_FREEHEAP, String(stats.available).c_str()); } @@ -1413,7 +1417,7 @@ void mqttFlush() { root[MQTT_TOPIC_MAC] = WiFi.macAddress(); #endif #if MQTT_ENQUEUE_HOSTNAME - root[MQTT_TOPIC_HOSTNAME] = getHostname(); + root[MQTT_TOPIC_HOSTNAME] = systemHostname(); #endif #if MQTT_ENQUEUE_IP root[MQTT_TOPIC_IP] = wifiStaIp().toString(); diff --git a/code/espurna/netbios.cpp b/code/espurna/netbios.cpp index ecf72c7f..ff5fb7b0 100644 --- a/code/espurna/netbios.cpp +++ b/code/espurna/netbios.cpp @@ -16,7 +16,7 @@ Copyright (C) 2017-2019 by Xose Pérez void netbiosSetup() { static WiFiEventHandler _netbios_wifi_onSTA = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP ipInfo) { - auto hostname = getHostname(); + const auto hostname = systemHostname(); NBNS.begin(hostname.c_str()); DEBUG_MSG_P(PSTR("[NETBIOS] Configured for %s\n"), hostname.c_str()); }); diff --git a/code/espurna/nofuss.cpp b/code/espurna/nofuss.cpp index 45d72407..0b630c30 100644 --- a/code/espurna/nofuss.cpp +++ b/code/espurna/nofuss.cpp @@ -59,19 +59,25 @@ void _nofussConfigure() { return; } - char device[256]; - sprintf_P(device, PSTR("%s_%s"), getAppName(), getDevice()); - - auto timestamp = String(__UNIX_TIMESTAMP__); NoFUSSClient.setServer(nofussServer); + + const auto info = buildInfo(); + String device; + device += info.app.name; + device += '_'; + device += info.hardware.device; NoFUSSClient.setDevice(device); - NoFUSSClient.setVersion(getVersion()); - NoFUSSClient.setBuild(timestamp); + + const auto version = String(info.app.version); + NoFUSSClient.setVersion(version); + + const auto time = String(buildTime()); + NoFUSSClient.setBuild(time); DEBUG_MSG_P(PSTR("[NOFUSS] Server: %s\n"), nofussServer.c_str()); - DEBUG_MSG_P(PSTR("[NOFUSS] Device: %s\n"), device); - DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), getVersion()); - DEBUG_MSG_P(PSTR("[NOFUSS] Build: %s\n"), timestamp.c_str()); + DEBUG_MSG_P(PSTR("[NOFUSS] Device: %s\n"), device.c_str()); + DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), version.c_str()); + DEBUG_MSG_P(PSTR("[NOFUSS] Build: %s\n"), time.c_str()); } // ----------------------------------------------------------------------------- diff --git a/code/espurna/ota_arduinoota.cpp b/code/espurna/ota_arduinoota.cpp index b3719983..8b87d0da 100644 --- a/code/espurna/ota_arduinoota.cpp +++ b/code/espurna/ota_arduinoota.cpp @@ -26,7 +26,7 @@ namespace { void configure() { ArduinoOTA.setPort(OTA_PORT); #if USE_PASSWORD - ArduinoOTA.setPassword(getAdminPass().c_str()); + ArduinoOTA.setPassword(systemPassword().c_str()); #endif ArduinoOTA.begin(false); } diff --git a/code/espurna/ota_httpupdate.cpp b/code/espurna/ota_httpupdate.cpp index b0c2e551..841b2e7a 100644 --- a/code/espurna/ota_httpupdate.cpp +++ b/code/espurna/ota_httpupdate.cpp @@ -38,10 +38,10 @@ namespace { #define _ota_client_trusted_root_ca _ssl_digicert_ev_root_ca #endif -#endif // SECURE_CLIENT != SECURE_CLIENT_NONE - } // namespace +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE + // ----------------------------------------------------------------------------- namespace ota { diff --git a/code/espurna/ota_web.cpp b/code/espurna/ota_web.cpp index 435e7ca0..57f39ade 100644 --- a/code/espurna/ota_web.cpp +++ b/code/espurna/ota_web.cpp @@ -57,7 +57,7 @@ void setStatus(AsyncWebServerRequest *request, int code, const String& payload = void onUpgrade(AsyncWebServerRequest *request) { if (!webAuthenticate(request)) { - return request->requestAuthentication(getHostname().c_str()); + return request->requestAuthentication(systemHostname().c_str()); } if (request->_tempObject) { @@ -69,7 +69,7 @@ void onUpgrade(AsyncWebServerRequest *request) { void onFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!webAuthenticate(request)) { - return request->requestAuthentication(getHostname().c_str()); + return request->requestAuthentication(systemHostname().c_str()); } // We set this after we are done with the request diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index d0166f93..1b8c07cc 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -1137,6 +1137,148 @@ bool realTimeValues() { } // namespace } // namespace settings + +alignas(4) static constexpr char List[] PROGMEM = +#if ADE7953_SUPPORT + "ADE7953 " +#endif +#if AM2320_SUPPORT + "AM2320_I2C " +#endif +#if ANALOG_SUPPORT + "ANALOG " +#endif +#if BH1750_SUPPORT + "BH1750 " +#endif +#if BMP180_SUPPORT + "BMP180 " +#endif +#if BMX280_SUPPORT + "BMX280 " +#endif +#if BME680_SUPPORT + "BME680 " +#endif +#if CSE7766_SUPPORT + "CSE7766 " +#endif +#if DALLAS_SUPPORT + "DALLAS " +#endif +#if DHT_SUPPORT + "DHTXX " +#endif +#if DIGITAL_SUPPORT + "DIGITAL " +#endif +#if ECH1560_SUPPORT + "ECH1560 " +#endif +#if EMON_ADC121_SUPPORT + "EMON_ADC121 " +#endif +#if EMON_ADS1X15_SUPPORT + "EMON_ADX1X15 " +#endif +#if EMON_ANALOG_SUPPORT + "EMON_ANALOG " +#endif +#if EVENTS_SUPPORT + "EVENTS " +#endif +#if GEIGER_SUPPORT + "GEIGER " +#endif +#if GUVAS12SD_SUPPORT + "GUVAS12SD " +#endif +#if HDC1080_SUPPORT + "HDC1080 " +#endif +#if HLW8012_SUPPORT + "HLW8012 " +#endif +#if INA219_SUPPORT + "INA219 " +#endif +#if LDR_SUPPORT + "LDR " +#endif +#if MAX6675_SUPPORT + "MAX6675 " +#endif +#if MHZ19_SUPPORT + "MHZ19 " +#endif +#if MICS2710_SUPPORT + "MICS2710 " +#endif +#if MICS5525_SUPPORT + "MICS5525 " +#endif +#if NTC_SUPPORT + "NTC " +#endif +#if PM1006_SUPPORT + "PM1006 " +#endif +#if PMSX003_SUPPORT + "PMSX003 " +#endif +#if PULSEMETER_SUPPORT + "PULSEMETER " +#endif +#if PZEM004T_SUPPORT + "PZEM004T " +#endif +#if PZEM004TV30_SUPPORT + "PZEM004TV30 " +#endif +#if SDS011_SUPPORT + "SDS011 " +#endif +#if SENSEAIR_SUPPORT + "SENSEAIR " +#endif +#if SHT3X_I2C_SUPPORT + "SHT3X_I2C " +#endif +#if SI7021_SUPPORT + "SI7021 " +#endif +#if SM300D2_SUPPORT + "SM300D2 " +#endif +#if SONAR_SUPPORT + "SONAR " +#endif +#if T6613_SUPPORT + "T6613 " +#endif +#if TMP3X_SUPPORT + "TMP3X " +#endif +#if V9261F_SUPPORT + "V9261F " +#endif +#if VEML6075_SUPPORT + "VEML6075 " +#endif +#if VL53L1X_SUPPORT + "VL53L1X " +#endif +#if EZOPH_SUPPORT + "EZOPH " +#endif +#if DUMMY_SENSOR_SUPPORT + "DUMMY " +#endif +#if SI1145_SUPPORT + "SI1145 " +#endif + ""; + } // namespace sensor namespace settings { @@ -4042,6 +4184,10 @@ espurna::sensor::Info magnitudeInfo(unsigned char index) { }; } +espurna::StringView sensorList() { + return espurna::sensor::List; +} + void sensorSetup() { espurna::sensor::setup(); } diff --git a/code/espurna/settings.cpp b/code/espurna/settings.cpp index b8cd023b..a4268c11 100644 --- a/code/espurna/settings.cpp +++ b/code/espurna/settings.cpp @@ -41,14 +41,22 @@ static kvs_type kv_store( namespace query { +const Setting* Setting::findFrom(const Setting* begin, const Setting* end, StringView key) { + for (auto it = begin; it != end; ++it) { + if ((*it) == key) { + return it; + } + } + + return end; +} + String Setting::findValueFrom(const Setting* begin, const Setting* end, StringView key) { String out; - for (auto it = begin; it != end; ++it) { - if ((*it) == key) { - out = (*it).value(); - break; - } + const auto value = findFrom(begin, end, key); + if (value != end) { + out = (*value).value(); } return out; @@ -552,6 +560,15 @@ String getSetting(const espurna::settings::Key& key, String&& defaultValue) { return std::move(defaultValue); } +String getSetting(const espurna::settings::Key& key, espurna::StringView defaultValue) { + auto result = espurna::settings::get(key.value()); + if (result) { + return std::move(result).get(); + } + + return String(defaultValue); +} + bool delSetting(const String& key) { return espurna::settings::del(key); } @@ -605,32 +622,42 @@ void resetSettings() { // ----------------------------------------------------------------------------- bool settingsRestoreJson(JsonObject& data) { - // Note: we try to match what /config generates, expect {"app":"ESPURNA",...} - const char* app = data["app"]; - if (!app || strcmp(app, getAppName()) != 0) { - DEBUG_MSG_P(PSTR("[SETTING] Wrong or missing 'app' key\n")); + const auto& app = data[F("app")]; + if (!app.success() || !app.is()) { + DEBUG_MSG_P(PSTR("[SETTING] Missing 'app' key\n")); + return false; + } + + const auto* data_app = app.as(); + const auto build_app = buildApp().name; + if (build_app != data_app) { + DEBUG_MSG_P(PSTR("[SETTING] Invalid 'app' key\n")); return false; } // .../config will add this key, but it is optional - if (data["backup"].as()) { + if (data[F("backup")].as()) { resetSettings(); } // These three are just metadata, no need to actually store them for (auto element : data) { - if (strcmp(element.key, "app") == 0) continue; - if (strcmp(element.key, "version") == 0) continue; - if (strcmp(element.key, "backup") == 0) continue; - setSetting(element.key, element.value.as()); + auto key = String(element.key); + if (key.startsWith(F("app")) + || key.startsWith(F("version")) + || key.startsWith(F("backup"))) + { + continue; + } + + setSetting(std::move(key), String(element.value.as())); } saveSettings(); DEBUG_MSG_P(PSTR("[SETTINGS] Settings restored successfully\n")); return true; - } bool settingsRestoreJson(char* json_string, size_t json_buffer_size) { diff --git a/code/espurna/settings.h b/code/espurna/settings.h index 69a3125b..d96e4400 100644 --- a/code/espurna/settings.h +++ b/code/espurna/settings.h @@ -237,8 +237,8 @@ String getSetting(const espurna::settings::Key& key); String getSetting(const espurna::settings::Key& key, const char* defaultValue); String getSetting(const espurna::settings::Key& key, const __FlashStringHelper* defaultValue); String getSetting(const espurna::settings::Key& key, const String& defaultValue); -String getSetting(const espurna::settings::Key& key, const String& defaultValue); String getSetting(const espurna::settings::Key& key, String&& defaultValue); +String getSetting(const espurna::settings::Key& key, espurna::StringView defaultValue); template ::type> T getSetting(const espurna::settings::Key& key, T defaultValue) { diff --git a/code/espurna/settings_helpers.h b/code/espurna/settings_helpers.h index da89e41c..741313e1 100644 --- a/code/espurna/settings_helpers.h +++ b/code/espurna/settings_helpers.h @@ -369,6 +369,13 @@ struct alignas(8) Setting { return _key == key; } + static const Setting* findFrom(const Setting* begin, const Setting* end, StringView key); + + template + static const Setting* findFrom(const T& settings, StringView key) { + return findFrom(std::begin(settings), std::end(settings), key); + } + static String findValueFrom(const Setting* begin, const Setting* end, StringView key); template diff --git a/code/espurna/ssdp.cpp b/code/espurna/ssdp.cpp index e03e397f..6e152fd4 100644 --- a/code/espurna/ssdp.cpp +++ b/code/espurna/ssdp.cpp @@ -21,7 +21,7 @@ namespace { namespace settings { String name() { - return getSetting("ssdpName", getHostname()); + return getSetting("ssdpName", systemHostname()); } // needs to be in the response @@ -34,12 +34,33 @@ String type() { String udn() { String out; out += F("38323636-4558-4dda-9188-cda0e6"); - out += String(ESP.getChipId(), 16); + out += String(systemShortChipId()); return out; } } // namespace settings +String entry(const String& tag, const String& value) { + String out; + out.reserve((tag.length() * 2) + value.length() + 16); + + out += '<'; + out += tag; + out += '>'; + + out += value; + + out += F("'; + + return out; +} + +String entry(const String& tag, espurna::StringView value) { + return entry(tag, String(value)); +} + String response() { String out; out.reserve(512); @@ -51,23 +72,6 @@ String response() { "0" ""); - auto entry = [](const String& tag, const String& value) -> String { - String out; - out.reserve((tag.length() * 2) + value.length() + 16); - - out += '<'; - out += tag; - out += '>'; - - out += value; - - out += F("'; - - return out; - }; - // http://%s:%u/ String base; base += F("http://"); @@ -91,22 +95,24 @@ String response() { device += entry(F("presentationURL"), String('/')); // %u - device += entry(F("serialNumber"), String(ESP.getChipId(), 10)); + device += entry(F("serialNumber"), systemShortChipId()); + + const auto app = buildApp(); // %s - device += entry(F("modelName"), getAppName()); + device += entry(F("modelName"), app.name); // %s - device += entry(F("modelNumber"), getVersion()); + device += entry(F("modelNumber"), app.version); // %s - device += entry(F("modelURL"), getAppWebsite()); + device += entry(F("modelURL"), app.website); // %s - device += entry(F("manufacturer"), getBoardName()); + device += entry(F("manufacturer"), systemDevice()); // %s - device += entry(F("manufacturerURL"), getAppWebsite()); + device += entry(F("manufacturerURL"), app.website); // uuid:38323636-4558-4dda-9188-cda0e6%06x device += entry(F("UDN"), settings::udn()); @@ -127,10 +133,12 @@ void setup() { SSDP.setDeviceType(settings::type()); SSDP.setSerialNumber(ESP.getChipId()); - SSDP.setModelName(getAppName()); - SSDP.setModelNumber(getVersion()); - SSDP.setModelURL(getAppWebsite()); - SSDP.setManufacturer(getBoardName()); + + const auto app = buildApp(); + SSDP.setModelName(String(app.name)); + SSDP.setModelNumber(String(app.version)); + SSDP.setModelURL(String(app.website)); + SSDP.setManufacturer(String(systemDevice())); SSDP.setURL("/"); SSDP.setName(settings::name()); diff --git a/code/espurna/system.cpp b/code/espurna/system.cpp index f7ae7ffa..16f40753 100644 --- a/code/espurna/system.cpp +++ b/code/espurna/system.cpp @@ -42,9 +42,10 @@ extern "C" unsigned long adc_rand_noise; namespace espurna { namespace system { +namespace { + namespace settings { namespace options { -namespace { alignas(4) static constexpr char None[] PROGMEM = "none"; alignas(4) static constexpr char Once[] PROGMEM = "once"; @@ -56,10 +57,19 @@ static constexpr espurna::settings::options::Enumeration Heartb {heartbeat::Mode::Repeat, Repeat}, }; -} // namespace } // namespace options + +namespace keys { + +alignas(4) static constexpr char Hostname[] PROGMEM = "hostname"; +alignas(4) static constexpr char Description[] PROGMEM = "desc"; +alignas(4) static constexpr char Password[] PROGMEM = "adminPass"; + +} // namespace keys + } // namespace settings } // namespace +} // namespace system namespace settings { namespace internal { @@ -121,6 +131,111 @@ uint32_t RandomDevice::operator()() const { return adc_rand_noise ^ *(reinterpret_cast(Address)); } +namespace { + +namespace internal { + +alignas(4) static constexpr char Hostname[] PROGMEM = HOSTNAME; +alignas(4) static constexpr char Password[] PROGMEM = ADMIN_PASS; + +} // namespace internal + +StringView chip_id() { + const static String out = ([]() { + const uint32_t regs[3] { + READ_PERI_REG(0x3ff00050), + READ_PERI_REG(0x3ff00054), + READ_PERI_REG(0x3ff0005c)}; + + uint8_t mac[6] { + 0xff, + 0xff, + 0xff, + static_cast((regs[1] >> 8ul) & 0xfful), + static_cast(regs[1] & 0xffu), + static_cast((regs[0] >> 24ul) & 0xffu)}; + + if (mac[2] != 0) { + mac[0] = (regs[2] >> 16ul) & 0xffu; + mac[1] = (regs[2] >> 8ul) & 0xffu; + mac[2] = (regs[2] & 0xffu); + } else if (0 == ((regs[1] >> 16ul) & 0xff)) { + mac[0] = 0x18; + mac[1] = 0xfe; + mac[2] = 0x34; + } else if (1 == ((regs[1] >> 16ul) & 0xff)) { + mac[0] = 0xac; + mac[1] = 0xd0; + mac[2] = 0x74; + } + + return hexEncode(mac); + })(); + + return out; +} + +StringView short_chip_id() { + const auto full = chip_id(); + return StringView(full.begin() + 6, full.end()); +} + +StringView device() { + const static String out = ([]() { + String out; + + const auto hardware = buildHardware(); + out.concat(hardware.manufacturer.c_str(), + hardware.manufacturer.length()); + out += '_'; + out.concat(hardware.device.c_str(), + hardware.device.length()); + + return out; + })(); + + return out; +} + +StringView identifier() { + const static String out = ([]() { + String out; + + const auto app = buildApp(); + out.concat(app.name.c_str(), app.name.length()); + + out += '-'; + + const auto id = short_chip_id(); + out.concat(id.c_str(), id.length()); + + return out; + })(); + + return out; +} + +String description() { + return getSetting(settings::keys::Description); +} + +String hostname() { + if (__builtin_strlen(internal::Hostname) > 0) { + return getSetting(settings::keys::Hostname, internal::Hostname); + } + + return getSetting(settings::keys::Hostname, identifier()); +} + +StringView default_password() { + return internal::Password; +} + +String password() { + return getSetting(settings::keys::Password, default_password()); +} + +} // namespace } // namespace system namespace time { @@ -437,6 +552,75 @@ bool check() { } // namespace stability #endif +void pre() { + // Some magic to allow seamless Tasmota OTA upgrades + // - inject dummy data sequence that is expected to hold current version info + // - purge settings, since we don't want accidentaly reading something as a kv + // - sometimes we cannot boot b/c of certain SDK params, purge last 16KiB + { + // ref. `SetOption78 1` in Tasmota + // - https://tasmota.github.io/docs/Commands/#setoptions (> SetOption78 Version check on Tasmota upgrade) + // - https://github.com/esphome/esphome/blob/0e59243b83913fc724d0229514a84b6ea14717cc/esphome/core/esphal.cpp#L275-L287 (the original idea from esphome) + // - https://github.com/arendst/Tasmota/blob/217addc2bb2cf46e7633c93e87954b245cb96556/tasmota/settings.ino#L218-L262 (specific checks, which succeed when finding 0xffffffff as version) + // - https://github.com/arendst/Tasmota/blob/0dfa38df89c8f2a1e582d53d79243881645be0b8/tasmota/i18n.h#L780-L782 (constants) + volatile uint32_t magic[] __attribute__((unused)) { + 0x5aa55aa5, + 0xffffffff, + 0xa55aa55a, + }; + + // ref. https://github.com/arendst/Tasmota/blob/217addc2bb2cf46e7633c93e87954b245cb96556/tasmota/settings.ino#L24 + // We will certainly find these when rebooting from Tasmota. Purge SDK as well, since we may experience WDT after starting up the softAP + auto* rtcmem = reinterpret_cast(RTCMEM_ADDR); + if ((0xA55A == rtcmem[64]) && (0xA55A == rtcmem[68])) { + DEBUG_MSG_P(PSTR("[MAIN] TASMOTA OTA, resetting...\n")); + rtcmem[64] = rtcmem[68] = 0; + customResetReason(CustomResetReason::Factory); + resetSettings(); + eraseSDKConfig(); + __builtin_trap(); + // can't return! + } + + // TODO: also check for things throughout the flash sector, somehow? + } + + // Workaround for SDK changes between 1.5.3 and 2.2.x or possible + // flash corruption happening to the 'default' WiFi config +#if SYSTEM_CHECK_ENABLED + if (!stability::check()) { + const uint32_t Address { ESP.getFlashChipSize() - (FLASH_SECTOR_SIZE * 3) }; + + static constexpr size_t PageSize { 256 }; +#ifdef FLASH_PAGE_SIZE + static_assert(FLASH_PAGE_SIZE == PageSize, ""); +#endif + static constexpr auto Alignment = alignof(uint32_t); + alignas(Alignment) std::array page; + + if (ESP.flashRead(Address, reinterpret_cast(page.data()), page.size())) { + constexpr uint32_t ConfigOffset { 0xb0 }; + + // In case flash was already erased at some point, but we are still here + alignas(Alignment) const std::array Empty { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + if (std::memcpy(&page[ConfigOffset], &Empty[0], Empty.size()) != 0) { + return; + } + + // 0x00B0: 0A 00 00 00 45 53 50 2D XX XX XX XX XX XX 00 00 ESP-XXXXXX + alignas(Alignment) const std::array Reference { 0xa0, 0x00, 0x00, 0x00, 0x45, 0x53, 0x50, 0x2d }; + if (std::memcmp(&page[ConfigOffset], &Reference[0], Reference.size()) != 0) { + DEBUG_MSG_P(PSTR("[MAIN] Invalid SDK config at 0x%08X, resetting...\n"), Address + ConfigOffset); + customResetReason(CustomResetReason::Factory); + systemForceStable(); + forceEraseSDKConfig(); + // can't return! + } + } + } +#endif +} + } // namespace boot // ----------------------------------------------------------------------------- @@ -856,6 +1040,8 @@ void loop() { } void setup() { + boot::pre(); + boot::hardware(); boot::customReason(); @@ -996,6 +1182,38 @@ espurna::duration::Seconds systemUptime() { return espurna::uptime(); } +espurna::StringView systemDevice() { + return espurna::system::device(); +} + +espurna::StringView systemIdentifier() { + return espurna::system::identifier(); +} + +espurna::StringView systemChipId() { + return espurna::system::chip_id(); +} + +espurna::StringView systemShortChipId() { + return espurna::system::short_chip_id(); +} + +espurna::StringView systemDefaultPassword() { + return espurna::system::default_password(); +} + +String systemPassword() { + return espurna::system::password(); +} + +String systemHostname() { + return espurna::system::hostname(); +} + +String systemDescription() { + return espurna::system::description(); +} + void systemSetup() { espurna::setup(); } diff --git a/code/espurna/system.h b/code/espurna/system.h index 7ae73a4f..c8c7fa43 100644 --- a/code/espurna/system.h +++ b/code/espurna/system.h @@ -52,7 +52,7 @@ struct RandomDevice { uint32_t operator()() const; }; -} // namespace random +} // namespace system namespace duration { @@ -335,4 +335,16 @@ bool systemHeartbeat(); espurna::duration::Seconds systemUptime(); +espurna::StringView systemDevice(); +espurna::StringView systemIdentifier(); + +espurna::StringView systemChipId(); +espurna::StringView systemShortChipId(); + +espurna::StringView systemDefaultPassword(); + +String systemPassword(); +String systemHostname(); +String systemDescription(); + void systemSetup(); diff --git a/code/espurna/telnet.cpp b/code/espurna/telnet.cpp index 3efb2fb5..d93f53a0 100644 --- a/code/espurna/telnet.cpp +++ b/code/espurna/telnet.cpp @@ -23,7 +23,7 @@ Updated to use WiFiServer and support reverse connections by Niek van der Maas < #include #include -#include "board.h" +#include "build.h" #include "crash.h" #include "telnet.h" #include "terminal.h" @@ -403,7 +403,7 @@ void _telnetData(unsigned char clientId, char * data, size_t len) { ? true : _telnetClientsAuth[clientId]; if (_telnetAuth && !authenticated) { - String password = getAdminPass(); + const auto password = systemPassword(); if (strncmp(data, password.c_str(), password.length()) == 0) { DEBUG_MSG_P(PSTR("[TELNET] Client #%d authenticated\n"), clientId); @@ -445,7 +445,7 @@ void _telnetNotifyConnected(unsigned char id) { } else { _telnetClientsAuth[id] = !_telnetAuth; if (_telnetAuth) { - if (getAdminPass().length()) { + if (systemPassword().length()) { _telnetWrite(id, "Password: "); } else { _telnetClientsAuth[id] = true; diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index a469009c..c2298e2b 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -242,24 +242,36 @@ void heap(CommandContext&& ctx) { } void uptime(CommandContext&& ctx) { - ctx.output.printf_P(PSTR("uptime %s\n"), getUptime().c_str()); + ctx.output.printf_P(PSTR("uptime %s\n"), + prettyDuration(systemUptime()).c_str()); terminalOK(ctx); } void info(CommandContext&& ctx) { + const auto app = buildApp(); ctx.output.printf_P(PSTR("%s %s built %s\n"), - getAppName(), getVersion(), buildTime().c_str()); - ctx.output.printf_P(PSTR("manufacturer: %s device: %s\n"), - getManufacturer(), getDevice()); + app.name.c_str(), app.version.c_str(), app.build_time.c_str()); + + ctx.output.printf_P(PSTR("device: %s\n"), + systemDevice().c_str()); ctx.output.printf_P(PSTR("mcu: esp8266 chipid: %s freq: %hhumhz\n"), - getFullChipId().c_str(), system_get_cpu_freq()); + systemChipId().c_str(), system_get_cpu_freq()); + + const auto sdk = buildSdk(); ctx.output.printf_P(PSTR("sdk: %s core: %s\n"), - ESP.getSdkVersion(), getCoreVersion().c_str()); + sdk.base.c_str(), sdk.version.c_str()); ctx.output.printf_P(PSTR("md5: %s\n"), ESP.getSketchMD5().c_str()); - ctx.output.printf_P(PSTR("support: %s\n"), getEspurnaModules()); + + const auto modules = buildModules(); + ctx.output.printf_P(PSTR("support: %.*s\n"), + modules.length(), modules.c_str()); + #if SENSOR_SUPPORT - ctx.output.printf_P(PSTR("sensors: %s\n"), getEspurnaSensors()); + const auto sensors = sensorList(); + ctx.output.printf_P(PSTR("sensors: %.*s\n"), + sensors.length(), sensors.c_str()); #endif + #if SYSTEM_CHECK_ENABLED ctx.output.printf_P(PSTR("system: %s boot counter: %u\n"), systemCheck() @@ -267,9 +279,11 @@ void info(CommandContext&& ctx) { : PSTR("UNSTABLE"), systemStabilityCounter()); #endif + #if DEBUG_SUPPORT crashResetReason(ctx.output); #endif + terminalOK(ctx); } @@ -370,10 +384,31 @@ private: uint32_t _sectors; }; +StringView flash_chip_mode() { + static const String out = ([]() -> String { + switch (ESP.getFlashChipMode()) { + case FM_DIO: + return PSTR("DIO"); + case FM_DOUT: + return PSTR("DOUT"); + case FM_QIO: + return PSTR("QIO"); + case FM_QOUT: + return PSTR("QOUT"); + case FM_UNKNOWN: + break; + } + + return PSTR("UNKNOWN"); + })(); + + return out; +} + void storage(CommandContext&& ctx) { ctx.output.printf_P(PSTR("flash chip ID: 0x%06X\n"), ESP.getFlashChipId()); ctx.output.printf_P(PSTR("speed: %u\n"), ESP.getFlashChipSpeed()); - ctx.output.printf_P(PSTR("mode: %s\n"), getFlashChipMode()); + ctx.output.printf_P(PSTR("mode: %s\n"), flash_chip_mode().c_str()); ctx.output.printf_P(PSTR("size: %u (SPI), %u (SDK)\n"), ESP.getFlashChipRealSize(), ESP.getFlashChipSize()); diff --git a/code/espurna/thingspeak.cpp b/code/espurna/thingspeak.cpp index 62c6b640..bf640b69 100644 --- a/code/espurna/thingspeak.cpp +++ b/code/espurna/thingspeak.cpp @@ -260,7 +260,8 @@ void send(WiFiClient& client, const URL& url, const String& data) { http.begin(client, url.host, url.port, url.path, url.protocol.equals(F("https"))); - http.addHeader(F("User-Agent"), getAppName()); + const auto app = buildApp(); + http.addHeader(F("User-Agent"), String(app.name)); http.addHeader(F("Content-Type"), F("application/x-www-form-urlencoded")); const auto response = http.POST(data); @@ -449,8 +450,10 @@ private: headers += F(" HTTP/1.1"); headers += F("\r\n"); + const auto app = buildApp(); + append(F("Host"), _address.host); - append(F("User-Agent"), getAppName()); + append(F("User-Agent"), String(app.name)); append(F("Connection"), F("close")); append(F("Content-Type"), F("application/x-www-form-urlencoded")); append(F("Content-Length"), String(_data.length(), 10)); @@ -676,13 +679,22 @@ void loop() { namespace web { namespace { +alignas(4) static constexpr char Prefix[] PROGMEM = "tspk"; + bool onKeyCheck(StringView key, const JsonVariant&) { - return espurna::settings::query::samePrefix(key, STRING_VIEW("tspk")); + return espurna::settings::query::samePrefix(key, Prefix); } void onVisible(JsonObject& root) { - if (haveRelaysOrSensors()) { - wsPayloadModule(root, PSTR("tspk")); + bool module { false }; +#if RELAY_SUPPORT + module = module || (relayCount() > 0); +#endif +#if SENSOR_SUPPORT + module = module || (magnitudeCount() > 0); +#endif + if (module) { + wsPayloadModule(root, Prefix); } } @@ -698,7 +710,7 @@ void onConnected(JsonObject& root) { } #if SENSOR_SUPPORT - sensorWebSocketMagnitudes(root, PSTR("tspk"), [](JsonArray& out, size_t index) { + sensorWebSocketMagnitudes(root, Prefix, [](JsonArray& out, size_t index) { out.add(settings::magnitude(index)); }); #endif diff --git a/code/espurna/types.h b/code/espurna/types.h index eef6eb47..debcf1b9 100644 --- a/code/espurna/types.h +++ b/code/espurna/types.h @@ -180,6 +180,10 @@ inline bool operator==(StringView lhs, StringView rhs) { return lhs.compare(rhs); } +inline bool operator!=(StringView lhs, StringView rhs) { + return !lhs.compare(rhs); +} + inline String operator+(String&& lhs, StringView rhs) { lhs.concat(rhs.c_str(), rhs.length()); return lhs; diff --git a/code/espurna/utils.cpp b/code/espurna/utils.cpp index fe2e2c00..9f2ebee7 100644 --- a/code/espurna/utils.cpp +++ b/code/espurna/utils.cpp @@ -8,7 +8,6 @@ Copyright (C) 2017-2019 by Xose Pérez #include "espurna.h" -#include "board.h" #include "ntp.h" #include @@ -26,105 +25,6 @@ bool tryParseId(const char* p, TryParseIdFunc limit, size_t& out) { return true; } -String getDescription() { - return getSetting("desc"); -} - -String getHostname() { - if (strlen(HOSTNAME) > 0) { - return getSetting("hostname", F(HOSTNAME)); - } - - return getSetting("hostname", getIdentifier()); -} - -void setDefaultHostname() { - if (!getSetting("hostname").length()) { - if (strlen(HOSTNAME) > 0) { - setSetting("hostname", F(HOSTNAME)); - } else { - setSetting("hostname", getIdentifier()); - } - } -} - -String getBoardName() { - return getSetting("boardName", F(DEVICE_NAME)); -} - -void setBoardName() { - if (!isEspurnaMinimal()) { - setSetting("boardName", F(DEVICE_NAME)); - } -} - -String getAdminPass() { - static const String defaultValue(F(ADMIN_PASS)); - return getSetting("adminPass", defaultValue); -} - -const String& getCoreVersion() { - static String version; - if (!version.length()) { -#ifdef ARDUINO_ESP8266_RELEASE - version = ESP.getCoreVersion(); - if (version.equals("00000000")) { - version = String(ARDUINO_ESP8266_RELEASE); - } - version.replace("_", "."); -#else -#define _GET_COREVERSION_STR(X) #X -#define GET_COREVERSION_STR(X) _GET_COREVERSION_STR(X) - version = GET_COREVERSION_STR(ARDUINO_ESP8266_GIT_DESC); -#undef _GET_COREVERSION_STR -#undef GET_COREVERSION_STR -#endif - } - return version; -} - -const String& getCoreRevision() { - static String revision; - if (!revision.length()) { -#ifdef ARDUINO_ESP8266_GIT_VER - revision = String(ARDUINO_ESP8266_GIT_VER, 16); -#else - revision = "(unspecified)"; -#endif - } - return revision; -} - -const char* getVersion() { - static const char version[] = APP_VERSION; - return version; -} - -const char* getAppName() { - static const char app[] = APP_NAME; - return app; -} - -const char* getAppAuthor() { - static const char author[] = APP_AUTHOR; - return author; -} - -const char* getAppWebsite() { - static const char website[] = APP_WEBSITE; - return website; -} - -const char* getDevice() { - static const char device[] = DEVICE; - return device; -} - -const char* getManufacturer() { - static const char manufacturer[] = MANUFACTURER; - return manufacturer; -} - String prettyDuration(espurna::duration::Seconds seconds) { time_t timestamp = static_cast(seconds.count()); tm spec; @@ -138,31 +38,6 @@ String prettyDuration(espurna::duration::Seconds seconds) { return String(buffer); } -String getUptime() { -#if NTP_SUPPORT - return prettyDuration(systemUptime()); -#else - return String(systemUptime().count(), 10); -#endif -} - -String buildTime() { -#if NTP_SUPPORT - constexpr const time_t ts = __UNIX_TIMESTAMP__; - tm timestruct; - gmtime_r(&ts, ×truct); - return ntpDateTime(×truct); -#else - char buffer[32]; - snprintf_P( - buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), - __TIME_YEAR__, __TIME_MONTH__, __TIME_DAY__, - __TIME_HOUR__, __TIME_MINUTE__, __TIME_SECOND__ - ); - return String(buffer); -#endif -} - // ----------------------------------------------------------------------------- // SSL // ----------------------------------------------------------------------------- @@ -483,29 +358,3 @@ size_t hexDecode(const char* in, size_t in_size, uint8_t* out, size_t out_size) uint8_t* out_ptr { hexDecode(in, in + in_size, out, out + out_size) }; return out_ptr - out; } - -const char* getFlashChipMode() { - static const char* mode { nullptr }; - if (!mode) { - switch (ESP.getFlashChipMode()) { - case FM_QIO: - mode = "QIO"; - break; - case FM_QOUT: - mode = "QOUT"; - break; - case FM_DIO: - mode = "DIO"; - break; - case FM_DOUT: - mode = "DOUT"; - break; - case FM_UNKNOWN: - default: - mode = "UNKNOWN"; - break; - } - } - - return mode; -} diff --git a/code/espurna/utils.h b/code/espurna/utils.h index 7b3a9159..46b2f383 100644 --- a/code/espurna/utils.h +++ b/code/espurna/utils.h @@ -12,29 +12,7 @@ Copyright (C) 2017-2019 by Xose Pérez #include "system.h" -void setDefaultHostname(); -void setBoardName(); - -const String& getCoreVersion(); -const String& getCoreRevision(); - -const char* getFlashChipMode(); -const char* getVersion(); -const char* getAppName(); -const char* getAppAuthor(); -const char* getAppWebsite(); -const char* getDevice(); -const char* getManufacturer(); - -String getDescription(); -String getHostname(); -String getAdminPass(); -String getBoardName(); -String buildTime(); -bool haveRelaysOrSensors(); - String prettyDuration(espurna::duration::Seconds); -String getUptime(); bool sslCheckFingerPrint(const char * fingerprint); bool sslFingerPrintArray(const char * fingerprint, unsigned char * bytearray); diff --git a/code/espurna/web.cpp b/code/espurna/web.cpp index 798d4e5e..67458d5b 100644 --- a/code/espurna/web.cpp +++ b/code/espurna/web.cpp @@ -237,7 +237,7 @@ namespace { bool _authenticateRequest(AsyncWebServerRequest* request) { #if USE_PASSWORD - return request->authenticate(WEB_USERNAME, getAdminPass().c_str()); + return request->authenticate(WEB_USERNAME, systemPassword().c_str()); #else return true; #endif @@ -258,7 +258,7 @@ bool _isAPModeRequest(AsyncWebServerRequest* request) { const auto host = header->value(); - const auto domain = getHostname() + '.'; + const auto domain = systemHostname() + '.'; const auto ip = wifiApIp().toString(); if (!host.equals(ip) && !host.startsWith(domain)) { @@ -290,7 +290,7 @@ bool _onAPModeRequest(AsyncWebServerRequest* request) { } void _webRequestAuth(AsyncWebServerRequest* request) { - request->requestAuthentication(getHostname().c_str(), true); + request->requestAuthentication(systemHostname().c_str(), true); } void _onReset(AsyncWebServerRequest *request) { @@ -304,20 +304,25 @@ void _onReset(AsyncWebServerRequest *request) { } void _onDiscover(AsyncWebServerRequest *request) { - const String device = getBoardName(); - const String hostname = getHostname(); + const auto app = buildApp(); - StaticJsonBuffer jsonBuffer; - JsonObject &root = jsonBuffer.createObject(); - root["app"] = getAppName(); - root["version"] = getVersion(); - root["device"] = device; - root["hostname"] = hostname.c_str(); + char buffer[256]; + int prefix_len = snprintf_P(buffer, sizeof(buffer), + PSTR("{\"hostname\":\"%s\"," + "\"device\":\"%s\"," + "\"app\":\"%s\"," + "\"version\": \"%s\"}"), + systemHostname().c_str(), + systemDevice().c_str(), + app.name.c_str(), + app.version.c_str()); - auto* response = request->beginResponseStream(F("application/json"), root.measureLength() + 1); - root.printTo(*response); + if (prefix_len <= 0) { + request->send(500); + return; + } - request->send(response); + request->send(200, F("application/json"), buffer); } void _onGetConfig(AsyncWebServerRequest *request) { @@ -329,10 +334,12 @@ void _onGetConfig(AsyncWebServerRequest *request) { auto out = std::make_shared(); out->reserve(TCP_MSS); + const auto app = buildApp(); + char buffer[256]; int prefix_len = snprintf_P(buffer, sizeof(buffer), PSTR("{\n\"app\": \"%s\",\n\"version\": \"%s\",\n\"backup\": \"1\""), - getAppName(), getVersion()); + app.name.c_str(), app.version.c_str()); if (prefix_len <= 0) { request->send(500); return; @@ -351,8 +358,6 @@ void _onGetConfig(AsyncWebServerRequest *request) { }); *out += "\n}"; - auto hostname = getHostname(); - AsyncWebServerResponse* response = request->beginChunkedResponse("application/json", [out](uint8_t* buffer, size_t maxLen, size_t index) -> size_t { auto len = out->length(); @@ -380,7 +385,7 @@ void _onGetConfig(AsyncWebServerRequest *request) { int written = snprintf_P(buffer, sizeof(buffer), PSTR("attachment; filename=\"%s %s backup.json\""), - hostname.c_str(), get_timestamp().c_str()); + systemHostname().c_str(), get_timestamp().c_str()); if (written > 0) { response->addHeader(F("Content-Disposition"), buffer); diff --git a/code/espurna/wifi.cpp b/code/espurna/wifi.cpp index 1da49356..aef6c3ec 100644 --- a/code/espurna/wifi.cpp +++ b/code/espurna/wifi.cpp @@ -1924,19 +1924,19 @@ wifi::ApMode mode() { } String defaultSsid() { - return getIdentifier(); + return String(systemIdentifier()); } String ssid() { return getSetting(FPSTR(keys::Ssid), build::hasSsid() ? build::ssid() - : getHostname()); + : systemHostname()); } String passphrase() { return getSetting(FPSTR(keys::Passphrase), build::hasPassphrase() ? build::passphrase() - : getAdminPass()); + : systemPassword()); } int8_t channel() { @@ -2767,7 +2767,7 @@ void loop() { case State::Connect: { if (!wifi::sta::connecting()) { - if (!wifi::sta::connection::start(getHostname())) { + if (!wifi::sta::connection::start(systemHostname())) { state = State::Timeout; break; } diff --git a/code/espurna/ws.cpp b/code/espurna/ws.cpp index d3ffa1dc..6a530d1b 100644 --- a/code/espurna/ws.cpp +++ b/code/espurna/ws.cpp @@ -482,7 +482,7 @@ bool _wsCheckKey(const String& key, const JsonVariant& value) { #endif if (key == STRING_VIEW("adminPass")) { - const auto pass = getAdminPass(); + const auto pass = systemPassword(); return !pass.equalsConstantTime(value.as()); } @@ -636,37 +636,49 @@ bool _wsOnKeyCheck(espurna::StringView key, const JsonVariant&) { void _wsOnConnected(JsonObject& root) { root[F("webMode")] = WEB_MODE_NORMAL; - root[F("app_name")] = getAppName(); - root[F("app_version")] = getVersion(); - root[F("app_build")] = buildTime(); - root[F("device")] = getDevice(); - root[F("manufacturer")] = getManufacturer(); - root[F("chipid")] = getFullChipId().c_str(); + const auto info = buildInfo(); + root[F("sdk")] = info.sdk.base.c_str(); + root[F("core")] = info.sdk.version.c_str(); + + // nb: flash strings are copied anyway, can't just use as a ptr. + // need to explicitly copy through our own ctor operator as `String`, + // we should not expect that the given view is actually a C-string + root[F("manufacturer")] = + String(info.hardware.manufacturer); + root[F("device")] = + String(info.hardware.device); + + root[F("app_name")] = + String(info.app.name); + root[F("app_version")] = + String(info.app.version); + root[F("app_build")] = info.app.build_time.c_str(); + + root[F("hostname")] = systemHostname(); + root[F("chipid")] = systemChipId().c_str(); + root[F("desc")] = systemDescription(); + root[F("bssid")] = WiFi.BSSIDstr(); root[F("channel")] = WiFi.channel(); - root[F("hostname")] = getHostname(); - root[F("desc")] = getDescription(); root[F("network")] = wifiStaSsid(); root[F("deviceip")] = wifiStaIp().toString(); root[F("sketch_size")] = ESP.getSketchSize(); root[F("free_size")] = ESP.getFreeSketchSpace(); - root[F("sdk")] = ESP.getSdkVersion(); - root[F("core")] = getCoreVersion(); root[F("webPort")] = getSetting(F("webPort"), espurna::web::ws::build::port()); root[F("wsAuth")] = getSetting(F("wsAuth"), espurna::web::ws::build::authentication()); } void _wsConnected(uint32_t client_id) { - + static const auto defaultPassword = String(systemDefaultPassword()); const bool changePassword = (USE_PASSWORD && WEB_FORCE_PASS_CHANGE) - ? getAdminPass().equals(ADMIN_PASS) + ? systemPassword().equalsConstantTime(defaultPassword) : false; if (changePassword) { StaticJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.createObject(); - root["webMode"] = WEB_MODE_PASSWORD; + root[F("webMode")] = WEB_MODE_PASSWORD; wsSend(client_id, root); return; } @@ -674,7 +686,6 @@ void _wsConnected(uint32_t client_id) { wsPostAll(client_id, _ws_callbacks.on_visible); wsPostSequence(client_id, _ws_callbacks.on_connected); wsPostSequence(client_id, _ws_callbacks.on_data); - } void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){