diff --git a/.gitattributes b/.gitattributes index a16ab2d4..b9ba4ddc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ *.gz.h -diff -merge *.gz.h linguist-generated=true +*.ini text eol=lf diff --git a/.travis.yml b/.travis.yml index 5262a75d..07dd77da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,15 +7,16 @@ cache: directories: - "~/.npm" - "~/.platformio" - - "$TRAVIS_BUILD_DIR/code/.piolibdeps" install: - pip install -U platformio +- pio platform update -p - npm install -g npm@latest - cd code && npm ci && cd .. env: global: - BUILDER_TOTAL_THREADS=4 - ESPURNA_PIO_PATCH_ISSUE_1610=y + - ESPURNA_PIO_SHARED_LIBRARIES=y script: - cd code && ./build.sh -p && cd .. stages: @@ -28,6 +29,7 @@ jobs: script: cd code && ./build.sh travis01 - script: cd code && ./build.sh travis02 - script: cd code && ./build.sh travis03 + - script: cd code && ./build.sh travis04 - stage: Release env: BUILDER_THREAD=0 - env: BUILDER_THREAD=1 diff --git a/code/.gitignore b/code/.gitignore index 079975bc..55db2a3b 100644 --- a/code/.gitignore +++ b/code/.gitignore @@ -1,15 +1,13 @@ -.clang_complete -core_version.h -custom.h -.DS_Store -.gcc-flags.json -.pioenvs -.piolibdeps -.python-version -.travis.yml -.vscode -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.pioenvs -.piolibdeps +.clang_complete +core_version.h +custom.h +.DS_Store +.gcc-flags.json +.python-version +.travis.yml +.vscode +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.pio +libraries/ diff --git a/code/build.sh b/code/build.sh index e0c09644..e7447465 100755 --- a/code/build.sh +++ b/code/build.sh @@ -112,19 +112,25 @@ build_webui() { echo "--------------------------------------------------------------" echo "Building web interface..." node node_modules/gulp/bin/gulp.js || exit + + # TODO: do something if webui files are different + # for now, just print in travis log + if ${TRAVIS:-false}; then + git --no-pager diff --stat + fi } build_environments() { echo "--------------------------------------------------------------" echo "Building firmware images..." - mkdir -p ../firmware/espurna-$version + mkdir -p $destination/espurna-$version for environment in $environments; do echo -n "* espurna-$version-$environment.bin --- " platformio run --silent --environment $environment || exit 1 - stat_bytes .pioenvs/$environment/firmware.bin + stat_bytes .pio/build/$environment/firmware.bin [[ "${TRAVIS_BUILD_STAGE_NAME}" = "Test" ]] || \ - mv .pioenvs/$environment/firmware.bin $destination/espurna-$version/espurna-$version-$environment.bin + mv .pio/build/$environment/firmware.bin $destination/espurna-$version/espurna-$version-$environment.bin done echo "--------------------------------------------------------------" } diff --git a/code/debug.sh b/code/debug.sh index 18f45cc9..67fbe1fc 100755 --- a/code/debug.sh +++ b/code/debug.sh @@ -49,7 +49,7 @@ done # check environment folder if [ ! -f $ELF ]; then - ELF=.pioenvs/$ENVIRONMENT/firmware.elf + ELF=.pio/build/$ENVIRONMENT/firmware.elf fi if [ ! -f $ELF ]; then echo "Could not find ELF file for the selected environment: $ELF" diff --git a/code/eagle.flash.1m0m1s.ld b/code/eagle.flash.1m0m1s.ld deleted file mode 100644 index 4102c750..00000000 --- a/code/eagle.flash.1m0m1s.ld +++ /dev/null @@ -1,19 +0,0 @@ -/* Flash Split for 1M chips, no SPIFFS, 1 sector for EEPROM */ -/* sketch 999KB */ -/* eeprom 4KB */ -/* reserved 16KB */ - -MEMORY -{ - dport0_0_seg : org = 0x3FF00000, len = 0x10 - dram0_0_seg : org = 0x3FFE8000, len = 0x14000 - iram1_0_seg : org = 0x40100000, len = 0x8000 - irom0_0_seg : org = 0x40201010, len = 0xf9ff0 -} - -PROVIDE ( _SPIFFS_start = 0x402FB000 ); -PROVIDE ( _SPIFFS_end = 0x402FB000 ); -PROVIDE ( _SPIFFS_page = 0x0 ); -PROVIDE ( _SPIFFS_block = 0x0 ); - -INCLUDE "../ld/eagle.app.v6.common.ld" diff --git a/code/eagle.flash.1m0m2s.ld b/code/eagle.flash.1m0m2s.ld deleted file mode 100644 index 069f337d..00000000 --- a/code/eagle.flash.1m0m2s.ld +++ /dev/null @@ -1,19 +0,0 @@ -/* Flash Split for 1M chips, no SPIFFS, 2 sectors for EEPROM */ -/* sketch 995KB */ -/* eeprom 8KB */ -/* reserved 16KB */ - -MEMORY -{ - dport0_0_seg : org = 0x3FF00000, len = 0x10 - dram0_0_seg : org = 0x3FFE8000, len = 0x14000 - iram1_0_seg : org = 0x40100000, len = 0x8000 - irom0_0_seg : org = 0x40201010, len = 0xf8ff0 -} - -PROVIDE ( _SPIFFS_start = 0x402FA000 ); -PROVIDE ( _SPIFFS_end = 0x402FA000 ); -PROVIDE ( _SPIFFS_page = 0x0 ); -PROVIDE ( _SPIFFS_block = 0x0 ); - -INCLUDE "../ld/eagle.app.v6.common.ld" diff --git a/code/eagle.flash.512k0m1s.ld b/code/eagle.flash.512k0m1s.ld deleted file mode 100644 index 79d17352..00000000 --- a/code/eagle.flash.512k0m1s.ld +++ /dev/null @@ -1,19 +0,0 @@ -/* Flash Split for 512K chips, no SPIFFS, 1 sector for EEPROM */ -/* sketch 487KB */ -/* eeprom 4KB */ -/* reserved 16KB */ - -MEMORY -{ - dport0_0_seg : org = 0x3FF00000, len = 0x10 - dram0_0_seg : org = 0x3FFE8000, len = 0x14000 - iram1_0_seg : org = 0x40100000, len = 0x8000 - irom0_0_seg : org = 0x40201010, len = 0x79ff0 -} - -PROVIDE ( _SPIFFS_start = 0x4027B000 ); -PROVIDE ( _SPIFFS_end = 0x4027B000 ); -PROVIDE ( _SPIFFS_page = 0x0 ); -PROVIDE ( _SPIFFS_block = 0x0 ); - -INCLUDE "../ld/eagle.app.v6.common.ld" diff --git a/code/espurna/alexa.ino b/code/espurna/alexa.ino index a8d4bc96..047a383a 100644 --- a/code/espurna/alexa.ino +++ b/code/espurna/alexa.ino @@ -23,12 +23,11 @@ static std::queue _alexa_queue; // ALEXA // ----------------------------------------------------------------------------- -bool _alexaWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _alexaWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "alexa", 5) == 0); } -void _alexaWebSocketOnSend(JsonObject& root) { - root["alexaVisible"] = 1; +void _alexaWebSocketOnConnected(JsonObject& root) { root["alexaEnabled"] = alexaEnabled(); root["alexaName"] = getSetting("alexaName"); } @@ -123,8 +122,10 @@ void alexaSetup() { #if WEB_SUPPORT webBodyRegister(_alexaBodyCallback); webRequestRegister(_alexaRequestCallback); - wsOnSendRegister(_alexaWebSocketOnSend); - wsOnReceiveRegister(_alexaWebSocketOnReceive); + wsRegister() + .onVisible([](JsonObject& root) { root["alexaVisible"] = 1; }) + .onConnected(_alexaWebSocketOnConnected) + .onKeyCheck(_alexaWebSocketOnKeyCheck); #endif // Register wifi callback diff --git a/code/espurna/api.ino b/code/espurna/api.ino index fac60d85..1c4fd1e9 100644 --- a/code/espurna/api.ino +++ b/code/espurna/api.ino @@ -22,12 +22,11 @@ std::vector _apis; // ----------------------------------------------------------------------------- -bool _apiWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _apiWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "api", 3) == 0); } -void _apiWebSocketOnSend(JsonObject& root) { - root["apiVisible"] = 1; +void _apiWebSocketOnConnected(JsonObject& root) { root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1; root["apiKey"] = getSetting("apiKey"); root["apiRealTime"] = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1; @@ -76,6 +75,47 @@ bool _asJson(AsyncWebServerRequest *request) { return asJson; } +void _onAPIsText(AsyncWebServerRequest *request) { + AsyncResponseStream *response = request->beginResponseStream("text/plain"); + String output; + output.reserve(48); + for (unsigned int i=0; i < _apis.size(); i++) { + output = ""; + output += _apis[i].key; + output += " -> "; + output += "/api/"; + output += _apis[i].key; + output += '\n'; + response->write(output.c_str()); + } + request->send(response); +} + +constexpr const size_t API_JSON_BUFFER_SIZE = 1024; + +void _onAPIsJson(AsyncWebServerRequest *request) { + + + DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE); + JsonObject& root = jsonBuffer.createObject(); + + constexpr const int BUFFER_SIZE = 48; + + for (unsigned int i=0; i < _apis.size(); i++) { + char buffer[BUFFER_SIZE] = {0}; + int res = snprintf(buffer, sizeof(buffer), "/api/%s", _apis[i].key); + if ((res < 0) || (res > (BUFFER_SIZE - 1))) { + request->send(500); + return; + } + root[_apis[i].key] = buffer; + } + AsyncResponseStream *response = request->beginResponseStream("application/json"); + root.printTo(*response); + request->send(response); + +} + void _onAPIs(AsyncWebServerRequest *request) { webLog(request); @@ -83,26 +123,11 @@ void _onAPIs(AsyncWebServerRequest *request) { bool asJson = _asJson(request); - char buffer[40]; - String output; if (asJson) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - for (unsigned int i=0; i < _apis.size(); i++) { - snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), _apis[i].key); - root[_apis[i].key] = String(buffer); - } - root.printTo(output); - jsonBuffer.clear(); - request->send(200, "application/json", output); - + _onAPIsJson(request); } else { - for (unsigned int i=0; i < _apis.size(); i++) { - snprintf_P(buffer, sizeof(buffer), PSTR("/api/%s"), _apis[i].key); - output += _apis[i].key + String(" -> ") + String(buffer) + String("\n"); - } - request->send(200, "text/plain", output); + _onAPIsText(request); } } @@ -220,8 +245,10 @@ void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f void apiSetup() { _apiConfigure(); - wsOnSendRegister(_apiWebSocketOnSend); - wsOnReceiveRegister(_apiWebSocketOnReceive); + wsRegister() + .onVisible([](JsonObject& root) { root["apiVisible"] = 1; }) + .onConnected(_apiWebSocketOnConnected) + .onKeyCheck(_apiWebSocketOnKeyCheck); webRequestRegister(_apiRequestCallback); espurnaRegisterReload(_apiConfigure); } diff --git a/code/espurna/button.ino b/code/espurna/button.ino index dcef559d..678fda74 100644 --- a/code/espurna/button.ino +++ b/code/espurna/button.ino @@ -36,7 +36,7 @@ void buttonMQTT(unsigned char id, uint8_t event) { #if WEB_SUPPORT -bool _buttonWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _buttonWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "btn", 3) == 0); } @@ -243,7 +243,7 @@ void buttonSetup() { // Websocket Callbacks #if WEB_SUPPORT - wsOnReceiveRegister(_buttonWebSocketOnReceive); + wsRegister().onKeyCheck(_buttonWebSocketOnKeyCheck); #endif // Register loop diff --git a/code/espurna/config/all.h b/code/espurna/config/all.h index 2494890c..1eef6bd5 100644 --- a/code/espurna/config/all.h +++ b/code/espurna/config/all.h @@ -28,6 +28,7 @@ #include "arduino.h" #include "hardware.h" #include "defaults.h" +#include "buildtime.h" #include "deprecated.h" #include "general.h" #include "dependencies.h" diff --git a/code/espurna/config/arduino.h b/code/espurna/config/arduino.h index 9f7bf595..f3c79868 100644 --- a/code/espurna/config/arduino.h +++ b/code/espurna/config/arduino.h @@ -11,6 +11,8 @@ //#define ALLNET_4DUINO_IOT_WLAN_RELAIS //#define ALLTERCO_SHELLY1 //#define ALLTERCO_SHELLY2 +//#define ALLTERCO_SHELLY1PM +//#define ALLTERCO_SHELLY25 //#define ARILUX_AL_LC01 //#define ARILUX_AL_LC02 //#define ARILUX_AL_LC02_V14 @@ -72,6 +74,7 @@ //#define ITEAD_SONOFF_RF //#define ITEAD_SONOFF_RFBRIDGE //#define ITEAD_SONOFF_S31 +//#define ITEAD_SONOFF_S31_LITE //#define ITEAD_SONOFF_SV //#define ITEAD_SONOFF_T1_1CH //#define ITEAD_SONOFF_T1_2CH @@ -81,6 +84,7 @@ //#define IWOOLE_LED_TABLE_LAMP //#define JANGOE_WIFI_RELAY_NC //#define JANGOE_WIFI_RELAY_NO +//#define JINVOO_VALVE_SM_AW713 //#define JORGEGARCIA_WIFI_RELAYS //#define KMC_70011 //#define LINGAN_SWA1 @@ -94,10 +98,12 @@ //#define MAGICHOME_LED_CONTROLLER_20 //#define MAGICHOME_ZJ_WFMN_A_11 //#define MAGICHOME_ZJ_WFMN_B_11 +//#define MAGICHOME_ZJ_ESPM_5CH_B_13 //#define MANCAVEMADE_ESPLIVE //#define MAXCIO_WDE004 //#define MAXCIO_WUS002S //#define NEO_COOLCAM_NAS_WR01W +//#define NEXETE_A19 //#define NODEMCU_BASIC //#define NODEMCU_LOLIN //#define OPENENERGYMONITOR_MQTT_RELAY @@ -124,6 +130,7 @@ //#define WION_50055 //#define WORKCHOICE_ECOPLUG //#define XENON_SM_PW702U +//#define ISELECTOR_SM_PW702 //#define XIAOMI_SMART_DESK_LAMP //#define YIDIAN_XSSSA05 //#define YJZK_SWITCH_1CH @@ -210,3 +217,4 @@ //#define V9261F_SUPPORT 1 //#define VEML6075_SUPPORT 1 //#define VL53L1X_SUPPORT 1 +//#define ADE7953_SUPPORT 1 diff --git a/code/espurna/config/buildtime.h b/code/espurna/config/buildtime.h new file mode 100644 index 00000000..a9b854d1 --- /dev/null +++ b/code/espurna/config/buildtime.h @@ -0,0 +1,97 @@ +/* + * + * Created: 29.03.2018 + * + * Authors: + * + * Assembled from the code released on Stackoverflow by: + * Dennis (instructable.com/member/nqtronix) | https://stackoverflow.com/questions/23032002/c-c-how-to-get-integer-unix-timestamp-of-build-time-not-string + * and + * Alexis Wilke | https://stackoverflow.com/questions/10538444/do-you-know-of-a-c-macro-to-compute-unix-time-and-date + * + * Assembled by Jean Rabault + * + * UNIX_TIMESTAMP gives the UNIX timestamp (unsigned long integer of seconds since 1st Jan 1970) of compilation from macros using the compiler defined __TIME__ macro. + * This should include Gregorian calendar leap days, in particular the 29ths of February, 100 and 400 years modulo leaps. + * + * Careful: __TIME__ is the local time of the computer, NOT the UTC time in general! + * + */ + +#ifndef COMPILE_TIME_H_ +#define COMPILE_TIME_H_ + +// Some definitions for calculation +#define SEC_PER_MIN 60UL +#define SEC_PER_HOUR 3600UL +#define SEC_PER_DAY 86400UL +#define SEC_PER_YEAR (SEC_PER_DAY*365) + +// extracts 1..4 characters from a string and interprets it as a decimal value +#define CONV_STR2DEC_1(str, i) (str[i]>'0'?str[i]-'0':0) +#define CONV_STR2DEC_2(str, i) (CONV_STR2DEC_1(str, i)*10 + str[i+1]-'0') +#define CONV_STR2DEC_3(str, i) (CONV_STR2DEC_2(str, i)*10 + str[i+2]-'0') +#define CONV_STR2DEC_4(str, i) (CONV_STR2DEC_3(str, i)*10 + str[i+3]-'0') + +// Custom "glue logic" to convert the month name to a usable number +#define GET_MONTH(str, i) (str[i]=='J' && str[i+1]=='a' && str[i+2]=='n' ? 1 : \ + str[i]=='F' && str[i+1]=='e' && str[i+2]=='b' ? 2 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='r' ? 3 : \ + str[i]=='A' && str[i+1]=='p' && str[i+2]=='r' ? 4 : \ + str[i]=='M' && str[i+1]=='a' && str[i+2]=='y' ? 5 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='n' ? 6 : \ + str[i]=='J' && str[i+1]=='u' && str[i+2]=='l' ? 7 : \ + str[i]=='A' && str[i+1]=='u' && str[i+2]=='g' ? 8 : \ + str[i]=='S' && str[i+1]=='e' && str[i+2]=='p' ? 9 : \ + str[i]=='O' && str[i+1]=='c' && str[i+2]=='t' ? 10 : \ + str[i]=='N' && str[i+1]=='o' && str[i+2]=='v' ? 11 : \ + str[i]=='D' && str[i+1]=='e' && str[i+2]=='c' ? 12 : 0) + +// extract the information from the time string given by __TIME__ and __DATE__ +#define __TIME_SECOND__ (CONV_STR2DEC_2(__TIME__, 6)) +#define __TIME_MINUTE__ (CONV_STR2DEC_2(__TIME__, 3)) +#define __TIME_HOUR__ (CONV_STR2DEC_2(__TIME__, 0)) +#define __TIME_DAY__ (CONV_STR2DEC_2(__DATE__, 4)) +#define __TIME_MONTH__ (GET_MONTH(__DATE__, 0)) +#define __TIME_YEAR__ (CONV_STR2DEC_4(__DATE__, 7)) + +// Days in February +#define _UNIX_TIMESTAMP_FDAY(year) \ + (((year) % 400) == 0UL ? 29UL : \ + (((year) % 100) == 0UL ? 28UL : \ + (((year) % 4) == 0UL ? 29UL : \ + 28UL))) + +// Days in the year +#define _UNIX_TIMESTAMP_YDAY(year, month, day) \ + ( \ + /* January */ day \ + /* February */ + (month >= 2 ? 31UL : 0UL) \ + /* March */ + (month >= 3 ? _UNIX_TIMESTAMP_FDAY(year) : 0UL) \ + /* April */ + (month >= 4 ? 31UL : 0UL) \ + /* May */ + (month >= 5 ? 30UL : 0UL) \ + /* June */ + (month >= 6 ? 31UL : 0UL) \ + /* July */ + (month >= 7 ? 30UL : 0UL) \ + /* August */ + (month >= 8 ? 31UL : 0UL) \ + /* September */+ (month >= 9 ? 31UL : 0UL) \ + /* October */ + (month >= 10 ? 30UL : 0UL) \ + /* November */ + (month >= 11 ? 31UL : 0UL) \ + /* December */ + (month >= 12 ? 30UL : 0UL) \ + ) + +// get the UNIX timestamp from a digits representation +#define _UNIX_TIMESTAMP(year, month, day, hour, minute, second) \ + ( /* time */ second \ + + minute * SEC_PER_MIN \ + + hour * SEC_PER_HOUR \ + + /* year day (month + day) */ (_UNIX_TIMESTAMP_YDAY(year, month, day) - 1) * SEC_PER_DAY \ + + /* year */ (year - 1970UL) * SEC_PER_YEAR \ + + ((year - 1969UL) / 4UL) * SEC_PER_DAY \ + - ((year - 1901UL) / 100UL) * SEC_PER_DAY \ + + ((year - 1601UL) / 400UL) * SEC_PER_DAY \ + ) + +// the UNIX timestamp +#define __UNIX_TIMESTAMP__ (_UNIX_TIMESTAMP(__TIME_YEAR__, __TIME_MONTH__, __TIME_DAY__, __TIME_HOUR__, __TIME_MINUTE__, __TIME_SECOND__)) + +#endif \ No newline at end of file diff --git a/code/espurna/config/defaults.h b/code/espurna/config/defaults.h index ec06f940..10dc9f92 100644 --- a/code/espurna/config/defaults.h +++ b/code/espurna/config/defaults.h @@ -552,6 +552,64 @@ #define LED8_RELAY 8 #endif +// ----------------------------------------------------------------------------- +// Digital Inputs +// ----------------------------------------------------------------------------- + +#ifndef DIGITAL1_PIN +#define DIGITAL1_PIN GPIO_NONE +#endif +#ifndef DIGITAL2_PIN +#define DIGITAL2_PIN GPIO_NONE +#endif +#ifndef DIGITAL3_PIN +#define DIGITAL3_PIN GPIO_NONE +#endif +#ifndef DIGITAL4_PIN +#define DIGITAL4_PIN GPIO_NONE +#endif +#ifndef DIGITAL5_PIN +#define DIGITAL5_PIN GPIO_NONE +#endif +#ifndef DIGITAL6_PIN +#define DIGITAL6_PIN GPIO_NONE +#endif +#ifndef DIGITAL7_PIN +#define DIGITAL7_PIN GPIO_NONE +#endif +#ifndef DIGITAL8_PIN +#define DIGITAL8_PIN GPIO_NONE +#endif + +// ----------------------------------------------------------------------------- +// Events +// ----------------------------------------------------------------------------- + +#ifndef EVENTS1_PIN +#define EVENTS1_PIN GPIO_NONE +#endif +#ifndef EVENTS2_PIN +#define EVENTS2_PIN GPIO_NONE +#endif +#ifndef EVENTS3_PIN +#define EVENTS3_PIN GPIO_NONE +#endif +#ifndef EVENTS4_PIN +#define EVENTS4_PIN GPIO_NONE +#endif +#ifndef EVENTS5_PIN +#define EVENTS5_PIN GPIO_NONE +#endif +#ifndef EVENTS6_PIN +#define EVENTS6_PIN GPIO_NONE +#endif +#ifndef EVENTS7_PIN +#define EVENTS7_PIN GPIO_NONE +#endif +#ifndef EVENTS8_PIN +#define EVENTS8_PIN GPIO_NONE +#endif + // ----------------------------------------------------------------------------- // General // ----------------------------------------------------------------------------- diff --git a/code/espurna/config/dependencies.h b/code/espurna/config/dependencies.h index a371ae75..ace75538 100644 --- a/code/espurna/config/dependencies.h +++ b/code/espurna/config/dependencies.h @@ -5,6 +5,11 @@ // Configuration settings are in the general.h file //------------------------------------------------------------------------------ +#if defined(ASYNC_TCP_SSL_ENABLED) && SECURE_CLIENT == SECURE_CLIENT_NONE +#undef SECURE_CLIENT +#define SECURE_CLIENT SECURE_CLIENT_AXTLS +#endif + #if DEBUG_TELNET_SUPPORT #undef TELNET_SUPPORT #define TELNET_SUPPORT 1 @@ -60,10 +65,10 @@ #define MQTT_SUPPORT 1 // If Home Assistant enabled enable MQTT #endif -#ifndef ASYNC_TCP_SSL_ENABLED +#if SECURE_CLIENT != SECURE_CLIENT_AXTLS #if THINGSPEAK_USE_SSL && THINGSPEAK_USE_ASYNC #undef THINGSPEAK_SUPPORT -#define THINGSPEAK_SUPPORT 0 // Thingspeak in ASYNC mode requires ASYNC_TCP_SSL_ENABLED +#define THINGSPEAK_SUPPORT 0 // Thingspeak in ASYNC mode requires SECURE_CLIENT_AXTLS #endif #endif @@ -76,3 +81,13 @@ #undef NTP_SUPPORT #define NTP_SUPPORT 1 // Scheduler needs NTP #endif + +#if (SECURE_CLIENT == SECURE_CLIENT_BEARSSL) +#undef OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE +#define OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE 0 // Use new HTTPUpdate API with BearSSL +#endif + +#if LWIP_VERSION_MAJOR != 1 +#undef MDNS_CLIENT_SUPPORT +#define MDNS_CLIENT_SUPPORT 0 // default resolver already handles this +#endif diff --git a/code/espurna/config/deprecated.h b/code/espurna/config/deprecated.h index a631b548..9480a0cf 100644 --- a/code/espurna/config/deprecated.h +++ b/code/espurna/config/deprecated.h @@ -13,3 +13,32 @@ #warning RF_PIN is deprecated! Please use RFB_RX_PIN instead #define RFB_RX_PIN RF_PIN #endif + +// 1.13.6 allow multiple digitals +#ifdef DIGITAL_PIN +#warning DIGITAL_PIN is deprecated! Please use DIGITAL1_PIN instead +#define DIGITAL1_PIN DIGITAL_PIN +#endif + +// 1.13.6 allow multiple events +#ifdef EVENTS_PIN +#warning EVENTS_PIN is deprecated! Please use EVENTS1_PIN instead +#define EVENTS1_PIN EVENTS_PIN +#endif + +// 1.13.6 unifies mqtt payload options +#ifdef HOMEASSISTANT_PAYLOAD_ON +#warning HOMEASSISTANT_PAYLOAD_ON is deprecated! Global RELAY_MQTT_ON is used instead +#endif + +#ifdef HOMEASSISTANT_PAYLOAD_OFF +#warning HOMEASSISTANT_PAYLOAD_OFF is deprecated! Global RELAY_MQTT_OFF is used instead +#endif + +#ifdef HOMEASSISTANT_PAYLOAD_AVAILABLE +#warning HOMEASSISTANT_PAYLOAD_AVAILABLE is deprecated! Global MQTT_STATUS_ONLINE is used instead +#endif + +#ifdef HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE +#warning HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE is deprecated! Global MQTT_STATUS_OFFLINE is used instead +#endif diff --git a/code/espurna/config/general.h b/code/espurna/config/general.h index 5670ea53..87f073e2 100644 --- a/code/espurna/config/general.h +++ b/code/espurna/config/general.h @@ -118,8 +118,17 @@ #define TELNET_AUTHENTICATION 1 // Request password to start telnet session by default #endif +#ifndef TELNET_PORT #define TELNET_PORT 23 // Port to listen to telnet clients +#endif + +#ifndef TELNET_MAX_CLIENTS #define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients +#endif + +#ifndef TELNET_SERVER +#define TELNET_SERVER TELNET_SERVER_ASYNC // Can be either TELNET_SERVER_ASYNC (using ESPAsyncTCP) or TELNET_SERVER_WIFISERVER (using WiFiServer) +#endif //------------------------------------------------------------------------------ // TERMINAL @@ -335,6 +344,10 @@ #define ENCODER_SUPPORT 0 #endif +#ifndef ENCODER_MINIMUM_DELTA +#define ENCODER_MINIMUM_DELTA 1 +#endif + //------------------------------------------------------------------------------ // LED //------------------------------------------------------------------------------ @@ -391,12 +404,17 @@ #define RELAY_REPORT_STATUS 1 #endif -// Configure the MQTT payload for ON/OFF +// Configure the MQTT payload for ON, OFF and TOGGLE +#ifndef RELAY_MQTT_OFF +#define RELAY_MQTT_OFF "0" +#endif + #ifndef RELAY_MQTT_ON #define RELAY_MQTT_ON "1" #endif -#ifndef RELAY_MQTT_OFF -#define RELAY_MQTT_OFF "0" + +#ifndef RELAY_MQTT_TOGGLE +#define RELAY_MQTT_TOGGLE "2" #endif // TODO Only single EEPROM address is used to store state, which is 1 byte @@ -485,6 +503,54 @@ #define WIFI2_DNS "" #endif +#ifndef WIFI3_SSID +#define WIFI3_SSID "" +#endif + +#ifndef WIFI3_PASS +#define WIFI3_PASS "" +#endif + +#ifndef WIFI3_IP +#define WIFI3_IP "" +#endif + +#ifndef WIFI3_GW +#define WIFI3_GW "" +#endif + +#ifndef WIFI3_MASK +#define WIFI3_MASK "" +#endif + +#ifndef WIFI3_DNS +#define WIFI3_DNS "" +#endif + +#ifndef WIFI4_SSID +#define WIFI4_SSID "" +#endif + +#ifndef WIFI4_PASS +#define WIFI4_PASS "" +#endif + +#ifndef WIFI4_IP +#define WIFI4_IP "" +#endif + +#ifndef WIFI4_GW +#define WIFI4_GW "" +#endif + +#ifndef WIFI4_MASK +#define WIFI4_MASK "" +#endif + +#ifndef WIFI4_DNS +#define WIFI4_DNS "" +#endif + #ifndef WIFI_RSSI_1M #define WIFI_RSSI_1M -30 // Calibrate it with your router reading the RSSI at 1m #endif @@ -493,6 +559,34 @@ #define WIFI_PROPAGATION_CONST 4 // This is typically something between 2.7 to 4.3 (free space is 2) #endif +// ref: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/kconfig.html#config-lwip-esp-gratuitous-arp +// ref: https://github.com/xoseperez/espurna/pull/1877#issuecomment-525612546 +// +// Broadcast gratuitous ARP periodically to update ARP tables on the AP and all devices on the same network. +// Helps to solve compatibility issues when ESP fails to timely reply to ARP requests, causing the device's ARP table entry to expire. + +#ifndef WIFI_GRATUITOUS_ARP_SUPPORT +#define WIFI_GRATUITOUS_ARP_SUPPORT 1 +#endif + +// Interval is randomized on each boot in range from ..._MIN to ..._MAX (ms) +#ifndef WIFI_GRATUITOUS_ARP_INTERVAL_MIN +#define WIFI_GRATUITOUS_ARP_INTERVAL_MIN 15000 +#endif + +#ifndef WIFI_GRATUITOUS_ARP_INTERVAL_MAX +#define WIFI_GRATUITOUS_ARP_INTERVAL_MAX 30000 +#endif + +// ref: https://github.com/esp8266/Arduino/issues/6471 +// ref: https://github.com/esp8266/Arduino/issues/6366 +// +// Issue #6366 turned out to be high tx power causing weird behavior. Lowering tx power achieved stability. +#ifndef WIFI_OUTPUT_POWER_DBM +#define WIFI_OUTPUT_POWER_DBM 20.0 +#endif + + // ----------------------------------------------------------------------------- // WEB // ----------------------------------------------------------------------------- @@ -506,7 +600,7 @@ #endif // This is not working at the moment!! -// Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0 +// Requires SECURE_CLIENT = SECURE_CLIENT_AXTLS and ESP8266 Arduino Core 2.4.0 #ifndef WEB_SSL_ENABLED #define WEB_SSL_ENABLED 0 // Use HTTPS web interface #endif @@ -574,7 +668,7 @@ #endif #ifndef API_BUFFER_SIZE -#define API_BUFFER_SIZE 15 // Size of the buffer for HTTP GET API responses +#define API_BUFFER_SIZE 64 // Size of the buffer for HTTP GET API responses #endif #ifndef API_REAL_TIME_VALUES @@ -621,19 +715,99 @@ #define SPIFFS_SUPPORT 0 // Do not add support for SPIFFS by default #endif +// ----------------------------------------------------------------------------- +// SSL Client ** EXPERIMENTAL ** +// ----------------------------------------------------------------------------- + +#ifndef SECURE_CLIENT +#define SECURE_CLIENT SECURE_CLIENT_NONE // What variant of WiFiClient to use + // SECURE_CLIENT_NONE - No secure client support (default) + // SECURE_CLIENT_AXTLS - axTLS client secure support (All Core versions, ONLY TLS 1.1) + // SECURE_CLIENT_BEARSSL - BearSSL client secure support (starting with 2.5.0, TLS 1.2) + // + // axTLS marked for derecation since Arduino Core 2.4.2 and **will** be removed in the future +#endif + +// Security check that is performed when the connection is established: +// SECURE_CLIENT_CHECK_CA - Use Trust Anchor / Root Certificate +// Supported only by the SECURE_CLIENT_BEARSSL +// (See respective ..._SECURE_CLIENT_INCLUDE_CA options per-module) +// SECURE_CLIENT_CHECK_FINGERPRINT - Check certificate fingerprint +// SECURE_CLIENT_CHECK_NONE - Allow insecure connections + +#ifndef SECURE_CLIENT_CHECK + +#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL +#define SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK_CA + +#else +#define SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK_FINGERPRINT + +#endif + + +#endif // SECURE_CLIENT_CHECK + +// Support Maximum Fragment Length Negotiation TLS extension +// "...negotiate a smaller maximum fragment length due to memory limitations or bandwidth limitations." +// - https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/bearssl-client-secure-class.html#mfln-or-maximum-fragment-length-negotiation-saving-ram +// - https://tools.ietf.org/html/rfc6066#section-4 +#ifndef SECURE_CLIENT_MFLN +#define SECURE_CLIENT_MFLN 0 // The only possible values are: 512, 1024, 2048 and 4096 + // Set to 0 to disable (default) +#endif + // ----------------------------------------------------------------------------- // OTA // ----------------------------------------------------------------------------- #ifndef OTA_PORT -#define OTA_PORT 8266 // OTA port +#define OTA_PORT 8266 // Port for ArduinoOTA #endif #ifndef OTA_MQTT_SUPPORT -#define OTA_MQTT_SUPPORT 0 // No support by default +#define OTA_MQTT_SUPPORT 0 // Listen for HTTP(s) URLs at '/ota'. Depends on OTA_CLIENT +#endif + +#ifndef OTA_ARDUINOOTA_SUPPORT +#define OTA_ARDUINOOTA_SUPPORT 1 // Support ArduinoOTA by default (4.2Kb) + // Implicitly depends on ESP8266mDNS library, thus increasing firmware size +#endif + +#ifndef OTA_CLIENT +#define OTA_CLIENT OTA_CLIENT_ASYNCTCP // Terminal / MQTT OTA support + // OTA_CLIENT_ASYNCTCP (ESPAsyncTCP library) + // OTA_CLIENT_HTTPUPDATE (Arduino Core library) +#endif + +#ifndef OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE +#define OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE 1 // Use old HTTPUpdate API by default +#endif + +#define OTA_GITHUB_FP "CA:06:F5:6B:25:8B:7A:0D:4F:2B:05:47:09:39:47:86:51:15:19:84" + +#ifndef OTA_FINGERPRINT +#define OTA_FINGERPRINT OTA_GITHUB_FP +#endif + +#ifndef OTA_SECURE_CLIENT_CHECK +#define OTA_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK +#endif + +#ifndef OTA_SECURE_CLIENT_MFLN +#define OTA_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN +#endif + +#ifndef OTA_SECURE_CLIENT_INCLUDE_CA +#define OTA_SECURE_CLIENT_INCLUDE_CA 0 // Use user-provided CA. Only PROGMEM PEM option is supported. + // TODO: eventually should be replaced with pre-parsed structs, read directly from flash + // (ref: https://github.com/earlephilhower/bearssl-esp8266/pull/14) + // + // When enabled, current implementation includes "static/ota_client_trusted_root_ca.h" with + // const char _ota_client_trusted_root_ca[] PROGMEM = "...PEM data..."; + // By default, using DigiCert root in "static/digicert_evroot_pem.h" (for https://github.com) #endif -#define OTA_GITHUB_FP "D7:9F:07:61:10:B3:92:93:E3:49:AC:89:84:5B:03:80:C1:9E:2F:8B" // ----------------------------------------------------------------------------- // NOFUSS @@ -698,26 +872,37 @@ #endif -#ifndef MQTT_USE_ASYNC -#define MQTT_USE_ASYNC 1 // Use AysncMQTTClient (1) or PubSubClient (0) +#ifndef MQTT_LIBRARY +#define MQTT_LIBRARY MQTT_LIBRARY_ASYNCMQTTCLIENT // MQTT_LIBRARY_ASYNCMQTTCLIENT (default, https://github.com/marvinroger/async-mqtt-client) + // MQTT_LIBRARY_PUBSUBCLIENT (https://github.com/knolleary/pubsubclient) + // MQTT_LIBRARY_ARDUINOMQTT (https://github.com/256dpi/arduino-mqtt) #endif +// ----------------------------------------------------------------------------- // MQTT OVER SSL -// Using MQTT over SSL works pretty well but generates problems with the web interface. -// It could be a good idea to use it in conjuntion with WEB_SUPPORT=0. -// Requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0. +// ----------------------------------------------------------------------------- +// +// Requires SECURE_CLIENT set to SECURE_CLIENT_AXTLS or SECURE_CLIENT_BEARSSL +// It is recommended to use MQTT_LIBRARY_ARDUINOMQTT or MQTT_LIBRARY_PUBSUBCLIENT +// It is recommended to use SECURE_CLIENT_BEARSSL +// It is recommended to use ESP8266 Arduino Core >= 2.5.2 with SECURE_CLIENT_BEARSSL +// +// Current version of MQTT_LIBRARY_ASYNCMQTTCLIENT only supports SECURE_CLIENT_AXTLS // -// You can use SSL with MQTT_USE_ASYNC=1 (AsyncMqttClient library) -// but you might experience hiccups on the web interface, so my recommendation is: -// WEB_SUPPORT=0 +// It is recommended to use WEB_SUPPORT=0 with either SECURE_CLIENT option, as there are miscellaneous problems when using them simultaneously +// (although, things might've improved, and I'd encourage to check whether this is true or not) // -// If you use SSL with MQTT_USE_ASYNC=0 (PubSubClient library) -// you will have to disable all the modules that use ESPAsyncTCP, that is: -// ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0, THINGSPEAK_SUPPORT=0 and WEB_SUPPORT=0 +// When using MQTT_LIBRARY_PUBSUBCLIENT or MQTT_LIBRARY_ARDUINOMQTT, you will have to disable every module that uses ESPAsyncTCP: +// ALEXA_SUPPORT=0, INFLUXDB_SUPPORT=0, TELNET_SUPPORT=0, THINGSPEAK_SUPPORT=0, DEBUG_TELNET_SUPPORT=0 and WEB_SUPPORT=0 +// Or, use "sync" versions instead (note that not every module has this option): +// THINGSPEAK_USE_ASYNC=0, TELNET_SERVER=TELNET_SERVER_WIFISERVER // -// You will need the fingerprint for your MQTT server, example for CloudMQTT: -// $ echo -n | openssl s_client -connect m11.cloudmqtt.com:24055 > cloudmqtt.pem -// $ openssl x509 -noout -in cloudmqtt.pem -fingerprint -sha1 +// See SECURE_CLIENT_CHECK for all possible connection verification options. +// +// The simpliest way to verify SSL connection is to use fingerprinting. +// For example, to get Google's MQTT server certificate fingerprint, run the following command: +// $ echo -n | openssl s_client -connect mqtt.googleapis.com:8883 2>&1 | openssl x509 -noout -fingerprint -sha1 | cut -d\= -f2 +// Note that fingerprint will change when certificate changes e.g. LetsEncrypt renewals or when the CSR updates #ifndef MQTT_SSL_ENABLED #define MQTT_SSL_ENABLED 0 // By default MQTT over SSL will not be enabled @@ -727,6 +912,20 @@ #define MQTT_SSL_FINGERPRINT "" // SSL fingerprint of the server #endif +#ifndef MQTT_SECURE_CLIENT_CHECK +#define MQTT_SECURE_CLIENT_CHECK SECURE_CLIENT_CHECK // Use global verification setting by default +#endif + +#ifndef MQTT_SECURE_CLIENT_MFLN +#define MQTT_SECURE_CLIENT_MFLN SECURE_CLIENT_MFLN // Use global MFLN setting by default +#endif + +#ifndef MQTT_SECURE_CLIENT_INCLUDE_CA +#define MQTT_SECURE_CLIENT_INCLUDE_CA 0 // Use user-provided CA. Only PROGMEM PEM option is supported. + // When enabled, current implementation includes "static/mqtt_client_trusted_root_ca.h" with + // const char _mqtt_client_trusted_root_ca[] PROGMEM = "...PEM data..."; + // By default, using LetsEncrypt X3 root in "static/letsencrypt_isrgroot_pem.h" +#endif #ifndef MQTT_ENABLED #define MQTT_ENABLED 0 // Do not enable MQTT connection by default @@ -846,7 +1045,9 @@ #define MQTT_TOPIC_TIMESTAMP "timestamp" #define MQTT_TOPIC_FREEHEAP "freeheap" #define MQTT_TOPIC_VCC "vcc" +#ifndef MQTT_TOPIC_STATUS #define MQTT_TOPIC_STATUS "status" +#endif #define MQTT_TOPIC_MAC "mac" #define MQTT_TOPIC_RSSI "rssi" #define MQTT_TOPIC_MESSAGE_ID "id" @@ -890,13 +1091,16 @@ #define MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX "notify_temp_range_max" +#ifndef MQTT_STATUS_ONLINE #define MQTT_STATUS_ONLINE "1" // Value for the device ON message +#endif + +#ifndef MQTT_STATUS_OFFLINE #define MQTT_STATUS_OFFLINE "0" // Value for the device OFF message (will) +#endif #define MQTT_ACTION_RESET "reboot" // RESET MQTT topic particle -#define MQTT_MESSAGE_ID_SHIFT 1000 // Store MQTT message id into EEPROM every these many - // Custom get and set postfixes // Use something like "/status" or "/set", with leading slash // Since 1.9.0 the default value is "" for getter and "/set" for setter @@ -956,15 +1160,18 @@ #define LIGHT_SAVE_DELAY 5 // Persist color after 5 seconds to avoid wearing out #endif +#ifndef LIGHT_MIN_PWM +#define LIGHT_MIN_PWM 0 +#endif #ifndef LIGHT_MAX_PWM #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #define LIGHT_MAX_PWM 255 -#endif - -#if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER +#elif LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER #define LIGHT_MAX_PWM 10000 // 10000 * 200ns => 2 kHz +#else +#define LIGHT_MAX_PWM 0 #endif #endif // LIGHT_MAX_PWM @@ -973,16 +1180,45 @@ #define LIGHT_LIMIT_PWM LIGHT_MAX_PWM // Limit PWM to this value (prevent 100% power) #endif +#ifndef LIGHT_MIN_VALUE +#define LIGHT_MIN_VALUE 0 // Minimum light value +#endif + #ifndef LIGHT_MAX_VALUE #define LIGHT_MAX_VALUE 255 // Maximum light value #endif +#ifndef LIGHT_MIN_BRIGHTNESS +#define LIGHT_MIN_BRIGHTNESS 0 // Minimum brightness value +#endif + #ifndef LIGHT_MAX_BRIGHTNESS -#define LIGHT_MAX_BRIGHTNESS 255 // Maximun brightness value +#define LIGHT_MAX_BRIGHTNESS 255 // Maximum brightness value #endif -#define LIGHT_MIN_MIREDS 153 // Default to the Philips Hue value that HA also use. -#define LIGHT_MAX_MIREDS 500 // https://developers.meethue.com/documentation/core-concepts +// Default mireds & kelvin to the Philips Hue limits +// https://developers.meethue.com/documentation/core-concepts +// +// Home Assistant also uses these, see Light::min_mireds, Light::max_mireds +// https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/light/__init__.py + +// Used when LIGHT_USE_WHITE AND LIGHT_USE_CCT is 1 - (1000000/Kelvin = MiReds) +// Warning! Don't change this yet, NOT FULLY IMPLEMENTED! +#ifndef LIGHT_COLDWHITE_MIRED +#define LIGHT_COLDWHITE_MIRED 153 // Coldwhite Strip, Value must be __BELOW__ W2!! (Default: 6535 Kelvin/153 MiRed) +#endif + +#ifndef LIGHT_WARMWHITE_MIRED +#define LIGHT_WARMWHITE_MIRED 500 // Warmwhite Strip, Value must be __ABOVE__ W1!! (Default: 2000 Kelvin/500 MiRed) +#endif + +#ifndef LIGHT_COLDWHITE_KELVIN +#define LIGHT_COLDWHITE_KELVIN 6536 +#endif + +#ifndef LIGHT_WARMWHITE_KELVIN +#define LIGHT_WARMWHITE_KELVIN 2000 +#endif #ifndef LIGHT_STEP #define LIGHT_STEP 32 // Step size @@ -1000,11 +1236,6 @@ #define LIGHT_USE_CCT 0 // Use the 5th channel as Coldwhite LEDs, LIGHT_USE_WHITE must be 1. #endif -// Used when LIGHT_USE_WHITE AND LIGHT_USE_CCT is 1 - (1000000/Kelvin = MiReds) -// Warning! Don't change this yet, NOT FULLY IMPLEMENTED! -#define LIGHT_COLDWHITE_MIRED 153 // Coldwhite Strip, Value must be __BELOW__ W2!! (Default: 6535 Kelvin/153 MiRed) -#define LIGHT_WARMWHITE_MIRED 500 // Warmwhite Strip, Value must be __ABOVE__ W1!! (Default: 2000 Kelvin/500 MiRed) - #ifndef LIGHT_USE_GAMMA #define LIGHT_USE_GAMMA 0 // Use gamma correction for color channels #endif @@ -1044,9 +1275,17 @@ #define DOMOTICZ_SUPPORT MQTT_SUPPORT // Build with domoticz (if MQTT) support (1.72Kb) #endif +#ifndef DOMOTICZ_ENABLED #define DOMOTICZ_ENABLED 0 // Disable domoticz by default +#endif + +#ifndef DOMOTICZ_IN_TOPIC #define DOMOTICZ_IN_TOPIC "domoticz/in" // Default subscription topic +#endif + +#ifndef DOMOTICZ_OUT_TOPIC #define DOMOTICZ_OUT_TOPIC "domoticz/out" // Default publication topic +#endif // ----------------------------------------------------------------------------- // HOME ASSISTANT @@ -1064,22 +1303,6 @@ #define HOMEASSISTANT_PREFIX "homeassistant" // Default MQTT prefix #endif -#ifndef HOMEASSISTANT_PAYLOAD_ON -#define HOMEASSISTANT_PAYLOAD_ON "1" // Payload for ON and available messages -#endif - -#ifndef HOMEASSISTANT_PAYLOAD_OFF -#define HOMEASSISTANT_PAYLOAD_OFF "0" // Payload for OFF and unavailable messages -#endif - -#ifndef HOMEASSISTANT_PAYLOAD_AVAILABLE -#define HOMEASSISTANT_PAYLOAD_AVAILABLE "1" // Payload for available messages -#endif - -#ifndef HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE -#define HOMEASSISTANT_PAYLOAD_NOT_AVAILABLE "0" // Payload for available messages -#endif - // ----------------------------------------------------------------------------- // INFLUXDB // ----------------------------------------------------------------------------- @@ -1139,7 +1362,7 @@ // THINGSPEAK OVER SSL // Using THINGSPEAK over SSL works well but generates problems with the web interface, // so you should compile it with WEB_SUPPORT to 0. -// When THINGSPEAK_USE_ASYNC is 1, requires ASYNC_TCP_SSL_ENABLED to 1 and ESP8266 Arduino Core 2.4.0. +// When THINGSPEAK_USE_ASYNC is 1, requires SECURE_CLIENT = SECURE_CLIENT_AXTLS and ESP8266 Arduino Core >= 2.4.0. #define THINGSPEAK_USE_SSL 0 // Use secure connection #define THINGSPEAK_FINGERPRINT "78 60 18 44 81 35 BF DF 77 84 D4 0A 22 0D 9B 4E 6C DC 57 2C" @@ -1278,6 +1501,8 @@ #endif // Enable RCSwitch support +// Originally implemented for SONOFF BASIC +// https://tinkerman.cat/adding-rf-to-a-non-rf-itead-sonoff/ // Also possible to use with SONOFF RF BRIDGE, thanks to @wildwiz // https://github.com/xoseperez/espurna/wiki/Hardware-Itead-Sonoff-RF-Bridge---Direct-Hack #ifndef RFB_DIRECT diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index e60e7b67..c686e109 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -20,11 +20,19 @@ // // Besides, other hardware specific information should be stated here +// ----------------------------------------------------------------------------- +// Custom hardware +// ----------------------------------------------------------------------------- + +#if defined(MANUFACTURER) and defined(DEVICE) + + // user has defined custom hardware, no need to check anything else + // ----------------------------------------------------------------------------- // ESPurna Core // ----------------------------------------------------------------------------- -#if defined(ESPURNA_CORE) +#elif defined(ESPURNA_CORE) // This is a special device targeted to generate a light-weight binary image // meant to be able to do two-step-updates: @@ -900,6 +908,25 @@ #define CSE7766_SUPPORT 1 #define CSE7766_PIN 1 +#elif defined(ITEAD_SONOFF_S31_LITE) + + // Info + #define MANUFACTURER "ITEAD" + #define DEVICE "SONOFF_S31_LITE" + + // Buttons + #define BUTTON1_PIN 0 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + + // Relays + #define RELAY1_PIN 12 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + // LEDs + #define LED1_PIN 13 + #define LED1_PIN_INVERSE 1 + #elif defined(ITEAD_SONOFF_IFAN02) // Info @@ -1297,6 +1324,37 @@ #define RFB_DIRECT 1 #define RFB_RX_PIN 4 +#elif defined(MAGICHOME_ZJ_ESPM_5CH_B_13) + + // Info + #define MANUFACTURER "MAGICHOME" + #define DEVICE "ZJ_ESPM_5CH_B_13" + #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT + #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER + #define DUMMY_RELAY_COUNT 1 + + // Buttons + #define BUTTON1_PIN 0 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + + // LEDs + #define LED1_PIN 2 + #define LED1_PIN_INVERSE 1 + + // Light + #define LIGHT_CHANNELS 5 + #define LIGHT_CH1_PIN 14 // RED + #define LIGHT_CH2_PIN 12 // GREEN + #define LIGHT_CH3_PIN 13 // BLUE + #define LIGHT_CH4_PIN 5 // COLD WHITE + #define LIGHT_CH5_PIN 15 // WARM WHITE + #define LIGHT_CH1_INVERSE 0 + #define LIGHT_CH2_INVERSE 0 + #define LIGHT_CH3_INVERSE 0 + #define LIGHT_CH4_INVERSE 0 + #define LIGHT_CH5_INVERSE 0 + // ----------------------------------------------------------------------------- // HUACANXING H801 & H802 // ----------------------------------------------------------------------------- @@ -1689,6 +1747,11 @@ #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER #define DUMMY_RELAY_COUNT 1 + // Buttons + #define BUTTON1_PIN 0 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + // Light #define LIGHT_CHANNELS 5 #define LIGHT_CH1_PIN 14 // RED @@ -1765,6 +1828,31 @@ #define LED1_PIN 4 #define LED1_PIN_INVERSE 1 +// ----------------------------------------------------------------------------- +// ISELECTOR SM-PW702 +// ----------------------------------------------------------------------------- + +#elif defined(ISELECTOR_SM_PW702) + + // Info + #define MANUFACTURER "ISELECTOR" + #define DEVICE "SM_PW702" + + // Buttons + #define BUTTON1_PIN 13 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + + // Relays + #define RELAY1_PIN 12 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + // LEDs + #define LED1_PIN 4 //BLUE + #define LED1_PIN_INVERSE 0 + #define LED2_PIN 5 //RED + #define LED2_PIN_INVERSE 1 + // ----------------------------------------------------------------------------- // AUTHOMETION LYT8266 // https://authometion.com/shop/en/home/13-lyt8266.html @@ -2079,6 +2167,9 @@ #define HLW8012_CURRENT_R 0.002 // Current resistor #define HLW8012_VOLTAGE_R_UP ( 2 * 1000000 ) // Upstream voltage resistor + // LED1 on RX pin + #define DEBUG_SERIAL_SUPPORT 1 + // ----------------------------------------------------------------------------- // Maxcio W-DE004 // ----------------------------------------------------------------------------- @@ -2438,8 +2529,9 @@ #ifndef DIGITAL_SUPPORT #define DIGITAL_SUPPORT 1 #endif - #define DIGITAL_PIN 16 - #define DIGITAL_PIN_MODE INPUT + #define DIGITAL1_PIN 16 + #define DIGITAL1_PIN_MODE INPUT + #define DIGITAL1_DEFAULT_STATE 0 // ----------------------------------------------------------------------------- // Heltec Touch Relay @@ -2497,11 +2589,11 @@ #define LED1_PIN 1 #define LED1_PIN_INVERSE 1 - // ----------------------------------------------------------------------------- - // Allnet 4duino ESP8266-UP-Relais - // http://www.allnet.de/de/allnet-brand/produkte/neuheiten/p/allnet-4duino-iot-wlan-relais-unterputz-esp8266-up-relais/ - // https://shop.allnet.de/fileadmin/transfer/products/148814.pdf - // ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// Allnet 4duino ESP8266-UP-Relais +// http://www.allnet.de/de/allnet-brand/produkte/neuheiten/p/allnet-4duino-iot-wlan-relais-unterputz-esp8266-up-relais/ +// https://shop.allnet.de/fileadmin/transfer/products/148814.pdf +// ----------------------------------------------------------------------------- #elif defined(ALLNET_4DUINO_IOT_WLAN_RELAIS) @@ -2807,6 +2899,9 @@ #define HLW8012_POWER_RATIO 3414290 #define HLW8012_INTERRUPT_ON FALLING + // BUTTON1 and LED1 are using Serial pins + #define DEBUG_SERIAL_SUPPORT 0 + // ----------------------------------------------------------------------------- // Teckin SP22 v1.4 - v1.6 // ----------------------------------------------------------------------------- @@ -2848,6 +2943,9 @@ #define HLW8012_POWER_RATIO 2533110 #define HLW8012_INTERRUPT_ON FALLING + // BUTTON1 and LED1 are using Serial pins + #define DEBUG_SERIAL_SUPPORT 0 + // ----------------------------------------------------------------------------- // Several boards under different names uing a power chip labelled BL0937 or HJL-01 // Also model number KS-602S @@ -2872,6 +2970,9 @@ #define LED1_PIN 1 #define LED1_PIN_INVERSE 1 + // LED1 is using TX pin + #define DEBUG_SERIAL_SUPPORT 0 + // ---------------------------------------------------------------------------------------- // Homecube 16A is similar but some pins differ and it also has RGB LEDs // https://www.amazon.de/gp/product/B07D7RVF56/ref=oh_aui_detailpage_o00_s01?ie=UTF8&psc=1 @@ -3054,7 +3155,93 @@ #define RELAY1_TYPE RELAY_TYPE_NORMAL #define RELAY2_PIN 5 #define RELAY2_TYPE RELAY_TYPE_NORMAL + + #elif defined(ALLTERCO_SHELLY1PM) + // Info + #define MANUFACTURER "ALLTERCO" + #define DEVICE "SHELLY1PM" + + // Buttons + #define BUTTON1_PIN 4 + #define BUTTON1_MODE BUTTON_SWITCH + #define BUTTON1_RELAY 1 + #define BUTTON2_PIN 2 + #define BUTTON2_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON2_LNGCLICK BUTTON_MODE_RESET + #define BUTTON2_LNGLNGCLICK BUTTON_MODE_FACTORY + + // Relays + #define RELAY1_PIN 15 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + // Light + #define LED1_PIN 0 + #define LED1_PIN_INVERSE 1 + + // HJL01 / BL0937 + #define HLW8012_SUPPORT 1 + #define HLW8012_SEL_PIN 12 + #define HLW8012_CF1_PIN 13 + #define HLW8012_CF_PIN 5 + + #define HLW8012_SEL_CURRENT LOW + #define HLW8012_CURRENT_RATIO 25740 + #define HLW8012_VOLTAGE_RATIO 313400 + #define HLW8012_POWER_RATIO 3414290 + #define HLW8012_INTERRUPT_ON FALLING + + //Temperature + #define NTC_SUPPORT 1 + #define SENSOR_SUPPORT 1 + #define NTC_BETA 3350 + #define NTC_R_UP 10000 + #define NTC_R_DOWN 0 + #define NTC_R0 8000 + + #elif defined(ALLTERCO_SHELLY25) + // Info + #define MANUFACTURER "ALLTERCO" + #define DEVICE "SHELLY25" + + // Buttons + #define BUTTON1_PIN 13 + #define BUTTON1_MODE BUTTON_SWITCH + #define BUTTON1_RELAY 1 + + #define BUTTON2_PIN 5 + #define BUTTON2_MODE BUTTON_SWITCH + #define BUTTON2_RELAY 2 + + #define BUTTON3_PIN 2 + #define BUTTON3_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON3_LNGCLICK BUTTON_MODE_RESET + #define BUTTON3_LNGLNGCLICK BUTTON_MODE_FACTORY + + // Relays + #define RELAY1_PIN 4 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + #define RELAY2_PIN 15 + #define RELAY2_TYPE RELAY_TYPE_NORMAL + + // Light + #define LED1_PIN 0 + #define LED1_PIN_INVERSE 1 + + //Temperature + #define NTC_SUPPORT 1 + #define SENSOR_SUPPORT 1 + #define NTC_BETA 3350 + #define NTC_R_UP 10000 + #define NTC_R_DOWN 0 + #define NTC_R0 8000 + + //Current + #define ADE7953_SUPPORT 1 + #define I2C_SDA_PIN 12 + #define I2C_SCL_PIN 14 + // ----------------------------------------------------------------------------- #elif defined(LOHAS_9W) @@ -3197,6 +3384,32 @@ #define LIGHT_CH3_INVERSE 0 #define LIGHT_CH4_INVERSE 0 + +// ----------------------------------------------------------------------------- +// Nexete A19 +// https://www.ebay.com/itm/Wifi-Smart-LED-light-Bulb-9W-60W-A19-850LM-RGBW-Dimmable-for-Alexa-Google-Home/283514779201 +// ----------------------------------------------------------------------------- + +#elif defined(NEXETE_A19) + + // Info + #define MANUFACTURER "NEXETE" + #define DEVICE "A19" + #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT + #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER + #define DUMMY_RELAY_COUNT 1 + + // Light + #define LIGHT_CHANNELS 4 + #define LIGHT_CH1_PIN 12 // RED + #define LIGHT_CH2_PIN 15 // GREEN + #define LIGHT_CH3_PIN 14 // BLUE + #define LIGHT_CH4_PIN 5 // WHITE + #define LIGHT_CH1_INVERSE 0 + #define LIGHT_CH2_INVERSE 0 + #define LIGHT_CH3_INVERSE 0 + #define LIGHT_CH4_INVERSE 0 + // ----------------------------------------------------------------------------- // Lombex Lux Nova 2 Tunable White // https://www.amazon.com/Lombex-Compatible-Equivalent-Dimmable-2700K-6500K/dp/B07B8K72PR @@ -3286,7 +3499,7 @@ // Relays #define RELAY1_PIN 15 #define RELAY1_TYPE RELAY_TYPE_NORMAL - + // Light RGBW #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER @@ -3449,22 +3662,22 @@ // Teckin SP20 // ----------------------------------------------------------------------------- - #elif defined(TECKIN_SP20) +#elif defined(TECKIN_SP20) - // Info + // Info #define MANUFACTURER "TECKIN" #define DEVICE "SP20" - // Buttons + // Buttons #define BUTTON1_PIN 13 #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH #define BUTTON1_RELAY 1 - // Relays + // Relays #define RELAY1_PIN 4 #define RELAY1_TYPE RELAY_TYPE_NORMAL - // LEDs + // LEDs #define LED1_PIN 2 #define LED1_PIN_INVERSE 1 #define LED2_PIN 0 @@ -3472,7 +3685,7 @@ #define LED2_MODE LED_MODE_FINDME #define LED2_RELAY 0 - // HJL01 / BL0937 + // HJL01 / BL0937 #ifndef HLW8012_SUPPORT #define HLW8012_SUPPORT 1 #endif @@ -3480,7 +3693,7 @@ #define HLW8012_CF1_PIN 14 #define HLW8012_CF_PIN 5 - #define HLW8012_SEL_CURRENT LOW + #define HLW8012_SEL_CURRENT LOW #define HLW8012_CURRENT_RATIO 25740 #define HLW8012_VOLTAGE_RATIO 313400 #define HLW8012_POWER_RATIO 3414290 @@ -3519,32 +3732,32 @@ // ----------------------------------------------------------------------------- #elif defined(PSH_WIFI_PLUG) - + // Info #define MANUFACTURER "PSH" #define DEVICE "WIFI_PLUG" - + // Relays #define RELAY1_PIN 2 #define RELAY1_TYPE RELAY_TYPE_NORMAL - + // LEDs #define LED1_PIN 0 #define LED1_PIN_INVERSE 0 - + #elif defined(PSH_RGBW_CONTROLLER) - + // Info #define MANUFACTURER "PSH" #define DEVICE "RGBW_CONTROLLER" #define RELAY_PROVIDER RELAY_PROVIDER_LIGHT #define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER #define DUMMY_RELAY_COUNT 1 - + // LEDs #define LED1_PIN 13 #define LED1_PIN_INVERSE 1 - + // Light #define LIGHT_CHANNELS 4 #define LIGHT_CH1_PIN 5 // RED @@ -3555,9 +3768,9 @@ #define LIGHT_CH2_INVERSE 0 #define LIGHT_CH3_INVERSE 0 #define LIGHT_CH4_INVERSE 0 - + #elif defined(PSH_WIFI_SENSOR) - + // Info #define MANUFACTURER "PSH" #define DEVICE "WIFI_SENSOR" @@ -3573,6 +3786,81 @@ #define LDR_ON_GROUND false #define LDR_RESISTOR 10000 +#elif defined(JINVOO_VALVE_SM_AW713) + + // Reflashing from original Tuya firmware + // to thirdparty firmware like espurna by: + // https://github.com/ct-Open-Source/tuya-convert + + // Info + #define MANUFACTURER "JINVOO" + #define DEVICE "VALVE_SM_AW713" + + // Buttons + #define BUTTON1_PIN 13 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + + // Relays + #define RELAY1_PIN 12 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + // LED + #define LED1_PIN 5 // 5 red led + #define LED1_PIN_INVERSE 0 + #define LED1_RELAY 1 + #define LED1_MODE LED_MODE_RELAY + + #define LED2_PIN 4 // 4 blue led + #define LED2_PIN_INVERSE 0 + #define LED2_RELAY 1 + #define LED2_MODE LED_MODE_FINDME_WIFI + +// ----------------------------------------------------------------------------- +// Etekcity ESW01-USA +// https://www.amazon.com/Etekcity-Voltson-Outlet-Monitoring-Required/dp/B01M3MYIFS +// ----------------------------------------------------------------------------- + +#elif defined(ETEKCITY_ESW01_USA) + + // Info + #define MANUFACTURER "ETEKCITY" + #define DEVICE "ESW01-USA" + + // Buttons + #define BUTTON1_PIN 14 + #define BUTTON1_MODE BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH + #define BUTTON1_RELAY 1 + + // Relays + #define RELAY1_PIN 4 + #define RELAY1_TYPE RELAY_TYPE_NORMAL + + // LEDs + // Blue + #define LED1_PIN 5 + #define LED1_PIN_INVERSE 0 + #define LED1_MODE LED_MODE_WIFI + // Yellow + #define LED2_PIN 16 + #define LED2_PIN_INVERSE 0 + #define LED2_MODE LED_MODE_FOLLOW + #define LED2_RELAY 1 + + // HLW8012 + #ifndef HLW8012_SUPPORT + #define HLW8012_SUPPORT 1 + #endif + #define HLW8012_SEL_PIN 15 + #define HLW8012_CF1_PIN 12 + #define HLW8012_CF_PIN 13 + + #define HLW8012_SEL_CURRENT HIGH // SEL pin to HIGH to measure current + #define HLW8012_CURRENT_R 0.001 // Current resistor + #define HLW8012_VOLTAGE_R_UP ( 4 * 470000 ) // Upstream voltage resistor + #define HLW8012_VOLTAGE_R_DOWN ( 1000 ) // Downstream voltage resistor + #define HLW8012_INTERRUPT_ON CHANGE + // ----------------------------------------------------------------------------- // TEST boards (do not use!!) // ----------------------------------------------------------------------------- @@ -3628,6 +3916,7 @@ #define LIGHT_CHANNELS 1 #define LIGHT_CH1_PIN 5 #define LIGHT_CH1_INVERSE 0 + #define ENCODER_SUPPORT 1 // A bit of HLW8012 - pins 6,7,8 #ifndef HLW8012_SUPPORT @@ -3655,13 +3944,13 @@ #define MICS2710_SUPPORT 1 #define MICS5525_SUPPORT 1 - // MAX6675 14 11 10 - #ifndef MAX6675_SUPPORT - #define MAX6675_SUPPORT 1 - #endif - #define MAX6675_CS_PIN 14 - #define MAX6675_SO_PIN 11 - #define MAX6675_SCK_PIN 10 +// MAX6675 14 11 10 +#ifndef MAX6675_SUPPORT +#define MAX6675_SUPPORT 1 +#endif +#define MAX6675_CS_PIN 14 +#define MAX6675_SO_PIN 11 +#define MAX6675_SCK_PIN 10 #elif defined(TRAVIS02) @@ -3703,7 +3992,7 @@ // A bit of EVENTS - pin 10 #define EVENTS_SUPPORT 1 - #define EVENTS_PIN 6 + #define EVENTS1_PIN 6 // Sonar #define SONAR_SUPPORT 1 @@ -3735,6 +4024,7 @@ #define INFLUXDB_SUPPORT 1 #define IR_SUPPORT 1 #define RF_SUPPORT 1 + #define OTA_MQTT_SUPPORT 1 #define RFB_DIRECT 1 #define RFB_RX_PIN 4 @@ -3766,8 +4056,8 @@ // will not work on real life since they all share GPIO // but it's OK to test build #define EMON_ANALOG_SUPPORT 1 - #define NTC_SENSOR 1 - #define LDR_SENSOR 1 + #define NTC_SUPPORT 1 + #define LDR_SUPPORT 1 #define PULSEMETER_SUPPORT 1 @@ -3777,12 +4067,9 @@ #define SSDP_SUPPORT 1 #define RF_SUPPORT 1 -#endif +#else -// ----------------------------------------------------------------------------- -// Check definitions -// ----------------------------------------------------------------------------- - -#if not defined(MANUFACTURER) || not defined(DEVICE) #error "UNSUPPORTED HARDWARE!!" + #endif + diff --git a/code/espurna/config/progmem.h b/code/espurna/config/progmem.h index 5f8649ab..1b8aaa35 100644 --- a/code/espurna/config/progmem.h +++ b/code/espurna/config/progmem.h @@ -135,6 +135,32 @@ PROGMEM const char espurna_modules[] = #endif ""; +PROGMEM const char espurna_ota_modules[] = + #if OTA_ARDUINOOTA_SUPPORT + "ARDUINO " + #endif + #if (OTA_CLIENT == OTA_CLIENT_ASYNCTCP) + "ASYNCTCP " + #endif + #if (OTA_CLIENT == OTA_CLIENT_HTTPUPDATE) + #if (SECURE_CLIENT == SECURE_CLIENT_NONE) + "*HTTPUPDATE " + #endif + #if (SECURE_CLIENT == SECURE_CLIENT_AXTLS) + "*HTTPUPDATE_AXTLS " + #endif + #if (SECURE_CLIENT == SECURE_CLIENT_BEARSSL) + "*HTTPUPDATE_BEARSSL " + #endif + #endif // OTA_CLIENT_HTTPUPDATE + #if OTA_MQTT_SUPPORT + "MQTT " + #endif + #if WEB_SUPPORT + "WEB " + #endif + ""; + //-------------------------------------------------------------------------------- // Sensors //-------------------------------------------------------------------------------- @@ -247,6 +273,9 @@ PROGMEM const char espurna_sensors[] = #if EZOPH_SUPPORT "EZOPH " #endif + #if ADE7953_SUPPORT + "ADE7953 " + #endif ""; diff --git a/code/espurna/config/prototypes.h b/code/espurna/config/prototypes.h index 74bfcc8b..265a14f6 100644 --- a/code/espurna/config/prototypes.h +++ b/code/espurna/config/prototypes.h @@ -1,7 +1,8 @@ #include #include #include -#include +#include +#include #include extern "C" { @@ -10,25 +11,75 @@ extern "C" { } #define UNUSED(x) (void)(x) +#define INLINE inline __attribute__((always_inline)) // ----------------------------------------------------------------------------- // System // ----------------------------------------------------------------------------- +#define LWIP_INTERNAL +#include +#undef LWIP_INTERNAL + +extern "C" { + #include + #include + #include + #include // ip_addr_t + #include // ERR_x + #include // dns_gethostbyname + #include // ip4/ip6 helpers + #include // LWIP_VERSION_MAJOR +} + uint32_t systemResetReason(); uint8_t systemStabilityCounter(); void systemStabilityCounter(uint8_t); +// ----------------------------------------------------------------------------- +// PROGMEM +// ----------------------------------------------------------------------------- + +#include + +// ref: https://github.com/esp8266/Arduino/blob/master/tools/sdk/libc/xtensa-lx106-elf/include/sys/pgmspace.h +// __STRINGIZE && __STRINGIZE_NX && PROGMEM definitions port + +// Do not replace macros unless running version older than 2.5.0 +#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) \ + || defined(ARDUINO_ESP8266_RELEASE_2_4_0) \ + || defined(ARDUINO_ESP8266_RELEASE_2_4_1) \ + || defined(ARDUINO_ESP8266_RELEASE_2_4_2) + +// Quoting esp8266/Arduino comments: +// "Since __section__ is supposed to be only use for global variables, +// there could be conflicts when a static/inlined function has them in the +// same file as a non-static PROGMEM object. +// Ref: https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html +// Place each progmem object into its own named section, avoiding conflicts" + +#define __TO_STR_(A) #A +#define __TO_STR(A) __TO_STR_(A) + +#undef PROGMEM +#define PROGMEM __attribute__((section( "\".irom.text." __FILE__ "." __TO_STR(__LINE__) "." __TO_STR(__COUNTER__) "\""))) + +// "PSTR() macro modified to start on a 32-bit boundary. This adds on average +// 1.5 bytes/string, but in return memcpy_P and strcpy_P will work 4~8x faster" +#undef PSTR +#define PSTR(s) (__extension__({static const char __c[] __attribute__((__aligned__(4))) PROGMEM = (s); &__c[0];})) + +#endif + // ----------------------------------------------------------------------------- // API // ----------------------------------------------------------------------------- + +using api_get_callback_f = std::function; +using api_put_callback_f = std::function ; + #if WEB_SUPPORT - typedef std::function api_get_callback_f; - typedef std::function api_put_callback_f; void apiRegister(const char * key, api_get_callback_f getFn, api_put_callback_f putFn = NULL); -#else - #define api_get_callback_f void * - #define api_put_callback_f void * #endif // ----------------------------------------------------------------------------- @@ -41,8 +92,10 @@ void systemStabilityCounter(uint8_t); // ----------------------------------------------------------------------------- // Debug // ----------------------------------------------------------------------------- -void debugSend(const char * format, ...); -void debugSend_P(PGM_P format, ...); + +#include "../libs/DebugSend.h" + +void debugSendImpl(const char*); extern "C" { void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); } @@ -66,6 +119,10 @@ extern "C" { #endif } +void infoMemory(const char* , unsigned int, unsigned int); +unsigned int getFreeHeap(); +unsigned int getInitialFreeHeap(); + // ----------------------------------------------------------------------------- // Domoticz // ----------------------------------------------------------------------------- @@ -89,6 +146,12 @@ bool gpioValid(unsigned char gpio); bool gpioGetLock(unsigned char gpio); bool gpioReleaseLock(unsigned char gpio); +// ----------------------------------------------------------------------------- +// Homeassistant +// ----------------------------------------------------------------------------- +struct ha_config_t; +void haSetup(); + // ----------------------------------------------------------------------------- // I2C // ----------------------------------------------------------------------------- @@ -114,21 +177,80 @@ int16_t i2c_read_int16(uint8_t address, uint8_t reg); int16_t i2c_read_int16_le(uint8_t address, uint8_t reg); void i2c_read_buffer(uint8_t address, uint8_t * buffer, size_t len); +// ----------------------------------------------------------------------------- +// Lights +// ----------------------------------------------------------------------------- + +unsigned char lightChannels(); + +void lightState(unsigned char i, bool state); +bool lightState(unsigned char i); + +void lightState(bool state); +bool lightState(); + +void lightBrightness(unsigned int brightness); +unsigned int lightBrightness(); + +unsigned int lightChannel(unsigned char id); +void lightChannel(unsigned char id, unsigned char value); + // ----------------------------------------------------------------------------- // MQTT // ----------------------------------------------------------------------------- -#if MQTT_SUPPORT - typedef std::function mqtt_callback_f; - void mqttRegister(mqtt_callback_f callback); - String mqttMagnitude(char * topic); -#else - #define mqtt_callback_f void * + +#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + #include + #include +#elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT + #include +#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT + #include #endif +using mqtt_callback_f = std::function; + +void mqttRegister(mqtt_callback_f callback); + +String mqttTopic(const char * magnitude, bool is_set); +String mqttTopic(const char * magnitude, unsigned int index, bool is_set); + +String mqttMagnitude(char * topic); + +void mqttSendRaw(const char * topic, const char * message, bool retain); +void mqttSendRaw(const char * topic, const char * message); + +void mqttSend(const char * topic, const char * message, bool force, bool retain); +void mqttSend(const char * topic, const char * message, bool force); +void mqttSend(const char * topic, const char * message); + +void mqttSend(const char * topic, unsigned int index, const char * message, bool force); +void mqttSend(const char * topic, unsigned int index, const char * message); + +const String& mqttPayloadOnline(); +const String& mqttPayloadOffline(); +const char* mqttPayloadStatus(bool status); + +void mqttSendStatus(); + // ----------------------------------------------------------------------------- // OTA // ----------------------------------------------------------------------------- -#include "ESPAsyncTCP.h" + +#include + +#if OTA_CLIENT == OTA_CLIENT_ASYNCTCP + #include +#endif + +#if OTA_CLIENT == OTA_CLIENT_HTTPUPDATE + #include + #include +#endif + +#if SECURE_CLIENT != SECURE_CLIENT_NONE + #include +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE // ----------------------------------------------------------------------------- // RFM69 @@ -148,10 +270,34 @@ typedef struct { // ----------------------------------------------------------------------------- #include +enum class RelayStatus : unsigned char { + OFF = 0, + ON = 1, + TOGGLE = 2, + UNKNOWN = 0xFF +}; + +RelayStatus relayParsePayload(const char * payload); + +bool relayStatus(unsigned char id, bool status, bool report, bool group_report); +bool relayStatus(unsigned char id, bool status); +bool relayStatus(unsigned char id); + +void relayToggle(unsigned char id, bool report, bool group_report); +void relayToggle(unsigned char id); + +unsigned char relayCount(); + +const String& relayPayloadOn(); +const String& relayPayloadOff(); +const String& relayPayloadToggle(); +const char* relayPayload(RelayStatus status); + // ----------------------------------------------------------------------------- // Settings // ----------------------------------------------------------------------------- #include + template bool setSetting(const String& key, T value); template bool setSetting(const String& key, unsigned int index, T value); template String getSetting(const String& key, T defaultValue); @@ -159,6 +305,17 @@ template String getSetting(const String& key, unsigned int index, T void settingsGetJson(JsonObject& data); bool settingsRestoreJson(JsonObject& data); +struct settings_cfg_t { + String& setting; + const char* key; + const char* default_value; +}; + +using settings_filter_t = std::function; +using settings_cfg_list_t = std::initializer_list; + +void settingsProcessConfig(const settings_cfg_list_t& config, settings_filter_t filter = nullptr); + // ----------------------------------------------------------------------------- // Terminal // ----------------------------------------------------------------------------- @@ -177,64 +334,117 @@ bool inline eraseSDKConfig(); #define ARRAYINIT(type, name, ...) type name[] = {__VA_ARGS__}; +size_t strnlen(const char*, size_t); +char* strnstr(const char*, const char*, size_t); + // ----------------------------------------------------------------------------- // WebServer // ----------------------------------------------------------------------------- + +class AsyncClient; +class AsyncWebServer; + #if WEB_SUPPORT #include AsyncWebServer * webServer(); #else - #define AsyncWebServerRequest void - #define ArRequestHandlerFunction void - #define AsyncWebSocketClient void - #define AsyncWebSocket void - #define AwsEventType void * + class AsyncWebServerRequest; + class ArRequestHandlerFunction; + class AsyncWebSocketClient; + class AsyncWebSocket; + class AwsEventType; #endif -typedef std::function web_body_callback_f; -typedef std::function web_request_callback_f; -void webBodyRegister(web_body_callback_f callback); -void webRequestRegister(web_request_callback_f callback); + +using web_body_callback_f = std::function; +using web_request_callback_f = std::function; +void webBodyRegister(web_body_callback_f); +void webRequestRegister(web_request_callback_f); // ----------------------------------------------------------------------------- // WebSockets // ----------------------------------------------------------------------------- +#include + +// TODO: pending configuration headers refactoring... here for now +struct ws_counter_t; +struct ws_data_t; +struct ws_debug_t; +struct ws_callbacks_t; + +using ws_on_send_callback_f = std::function; +using ws_on_action_callback_f = std::function; +using ws_on_keycheck_callback_f = std::function; + +using ws_on_send_callback_list_t = std::vector; +using ws_on_action_callback_list_t = std::vector; +using ws_on_keycheck_callback_list_t = std::vector; + #if WEB_SUPPORT - typedef std::function ws_on_send_callback_f; - void wsOnSendRegister(ws_on_send_callback_f callback); - void wsSend(uint32_t, JsonObject& root); + struct ws_callbacks_t { + ws_on_send_callback_list_t on_visible; + ws_on_send_callback_list_t on_connected; + ws_on_send_callback_list_t on_data; + + ws_on_action_callback_list_t on_action; + ws_on_keycheck_callback_list_t on_keycheck; + + ws_callbacks_t& onVisible(ws_on_send_callback_f); + ws_callbacks_t& onConnected(ws_on_send_callback_f); + ws_callbacks_t& onData(ws_on_send_callback_f); + ws_callbacks_t& onAction(ws_on_action_callback_f); + ws_callbacks_t& onKeyCheck(ws_on_keycheck_callback_f); + }; + + ws_callbacks_t& wsRegister(); + + void wsSetup(); + void wsSend(uint32_t client_id, const char* data); + void wsSend(uint32_t client_id, JsonObject& root); void wsSend(JsonObject& root); - void wsSend(ws_on_send_callback_f sender); + void wsSend(ws_on_send_callback_f callback); - typedef std::function ws_on_action_callback_f; - void wsOnActionRegister(ws_on_action_callback_f callback); + void wsSend_P(PGM_P data); + void wsSend_P(uint32_t client_id, PGM_P data); - typedef std::function ws_on_receive_callback_f; - void wsOnReceiveRegister(ws_on_receive_callback_f callback); + void INLINE wsPost(const ws_on_send_callback_f& callback); + void INLINE wsPost(uint32_t client_id, const ws_on_send_callback_f& callback); + void INLINE wsPost(const ws_on_send_callback_list_t& callbacks); + void INLINE wsPost(uint32_t client_id, const ws_on_send_callback_list_t& callbacks); - bool wsConnected(); - bool wsConnected(uint32_t); - bool wsDebugSend(const char*, const char*); -#else - #define ws_on_send_callback_f void * - #define ws_on_action_callback_f void * - #define ws_on_receive_callback_f void * + void INLINE wsPostAll(uint32_t client_id, const ws_on_send_callback_list_t& callbacks); + void INLINE wsPostAll(const ws_on_send_callback_list_t& callbacks); + + void INLINE wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& callbacks); + void INLINE wsPostSequence(uint32_t client_id, ws_on_send_callback_list_t&& callbacks); + void INLINE wsPostSequence(const ws_on_send_callback_list_t& callbacks); + + bool INLINE wsConnected(); + bool INLINE wsConnected(uint32_t client_id); + bool wsDebugSend(const char* prefix, const char* message); #endif // ----------------------------------------------------------------------------- // WIFI // ----------------------------------------------------------------------------- -#include "JustWifi.h" -typedef std::function wifi_callback_f; +#include +struct wifi_scan_info_t; +using wifi_scan_f = std::function; +using wifi_callback_f = std::function; void wifiRegister(wifi_callback_f callback); bool wifiConnected(); +#if LWIP_VERSION_MAJOR == 1 +#include +#else // LWIP_VERSION_MAJOR >= 2 +#include +#endif + +// ----------------------------------------------------------------------------- // THERMOSTAT // ----------------------------------------------------------------------------- +using thermostat_callback_f = std::function; #if THERMOSTAT_SUPPORT - typedef std::function thermostat_callback_f; void thermostatRegister(thermostat_callback_f callback); -#else - #define thermostat_callback_f void * #endif // ----------------------------------------------------------------------------- @@ -242,3 +452,25 @@ bool wifiConnected(); // ----------------------------------------------------------------------------- #include "rtcmem.h" +// ----------------------------------------------------------------------------- +// Warn about broken Arduino functions +// ----------------------------------------------------------------------------- + +// Division by zero bug +// https://github.com/esp8266/Arduino/pull/2397 +// https://github.com/esp8266/Arduino/pull/2408 +#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) +long __attribute__((deprecated("Please avoid using map() with Core 2.3.0"))) map(long x, long in_min, long in_max, long out_min, long out_max); +#endif + +// ----------------------------------------------------------------------------- +// std::make_unique backport for C++11 +// ----------------------------------------------------------------------------- +#if 201103L >= __cplusplus +namespace std { + template + std::unique_ptr make_unique(Args&&... args) { + return std::unique_ptr(new T(std::forward(args)...)); + } +} +#endif diff --git a/code/espurna/config/rtcmem.h b/code/espurna/config/rtcmem.h index ae58ae59..f5108257 100644 --- a/code/espurna/config/rtcmem.h +++ b/code/espurna/config/rtcmem.h @@ -33,7 +33,7 @@ struct RtcmemData { uint32_t relay; uint32_t mqtt; uint64_t light; - double energy; + double energy[4]; }; static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big"); diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h index 24cd9929..d5796357 100644 --- a/code/espurna/config/sensors.h +++ b/code/espurna/config/sensors.h @@ -167,11 +167,18 @@ // (BMX280_ADDRESS == 0x00) then sensor #1 is auto-discovered // (BMX280_ADDRESS != 0x00) then sensor #1 is the unnamed address +#ifndef BMX280_MODE #define BMX280_MODE 1 // 0 for sleep mode, 1 or 2 for forced mode, 3 for normal mode +#endif +#ifndef BMX280_STANDBY #define BMX280_STANDBY 0 // 0 for 0.5ms, 1 for 62.5ms, 2 for 125ms // 3 for 250ms, 4 for 500ms, 5 for 1000ms // 6 for 10ms, 7 for 20ms +#endif +#ifndef BMX280_FILTER #define BMX280_FILTER 0 // 0 for OFF, 1 for 2 values, 2 for 4 values, 3 for 8 values and 4 for 16 values +#endif +#ifndef BMX280_TEMPERATURE #define BMX280_TEMPERATURE 1 // Oversampling for temperature (set to 0 to disable magnitude) // 0b000 = 0 = Skip measurement // 0b001 = 1 = 1x 16bit/0.0050C resolution @@ -179,6 +186,8 @@ // 0b011 = 3 = 4x 18bit/0.0012C // 0b100 = 4 = 8x 19bit/0.0006C // 0b101 = 5 = 16x 20bit/0.0003C +#endif +#ifndef BMX280_HUMIDITY #define BMX280_HUMIDITY 1 // Oversampling for humidity (set to 0 to disable magnitude, only for BME280) // 0b000 = 0 = Skip measurement // 0b001 = 1 = 1x 0.07% resolution @@ -186,6 +195,8 @@ // 0b011 = 3 = 4x 0.04% // 0b100 = 4 = 8x 0.03% // 0b101 = 5 = 16x 0.02% +#endif +#ifndef BMX280_PRESSURE #define BMX280_PRESSURE 1 // Oversampling for pressure (set to 0 to disable magnitude) // 0b000 = 0 = Skipped // 0b001 = 1 = 1x 16bit/2.62 Pa resolution @@ -193,7 +204,8 @@ // 0b011 = 3 = 4x 18bit/0.66 Pa // 0b100 = 4 = 8x 19bit/0.33 Pa // 0b101 = 5 = 16x 20bit/0.16 Pa - +#endif + //------------------------------------------------------------------------------ // Dallas OneWire temperature sensors // Enable support by passing DALLAS_SUPPORT=1 build flag @@ -260,16 +272,100 @@ #define DIGITAL_SUPPORT 0 #endif -#ifndef DIGITAL_PIN -#define DIGITAL_PIN 2 +#ifndef DIGITAL1_PIN +#define DIGITAL1_PIN 2 +#endif + +#ifndef DIGITAL1_PIN_MODE +#define DIGITAL1_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL1_DEFAULT_STATE +#define DIGITAL1_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL2_PIN +#define DIGITAL2_PIN 2 +#endif + +#ifndef DIGITAL2_PIN_MODE +#define DIGITAL2_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL2_DEFAULT_STATE +#define DIGITAL2_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL3_PIN +#define DIGITAL3_PIN 2 +#endif + +#ifndef DIGITAL3_PIN_MODE +#define DIGITAL3_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL3_DEFAULT_STATE +#define DIGITAL3_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL4_PIN +#define DIGITAL4_PIN 2 +#endif + +#ifndef DIGITAL4_PIN_MODE +#define DIGITAL4_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL4_DEFAULT_STATE +#define DIGITAL4_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL5_PIN +#define DIGITAL5_PIN 2 +#endif + +#ifndef DIGITAL5_PIN_MODE +#define DIGITAL5_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL5_DEFAULT_STATE +#define DIGITAL5_DEFAULT_STATE 1 #endif -#ifndef DIGITAL_PIN_MODE -#define DIGITAL_PIN_MODE INPUT_PULLUP +#ifndef DIGITAL6_PIN +#define DIGITAL6_PIN 2 #endif -#ifndef DIGITAL_DEFAULT_STATE -#define DIGITAL_DEFAULT_STATE 1 +#ifndef DIGITAL6_PIN_MODE +#define DIGITAL6_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL6_DEFAULT_STATE +#define DIGITAL6_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL7_PIN +#define DIGITAL7_PIN 2 +#endif + +#ifndef DIGITAL7_PIN_MODE +#define DIGITAL7_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL7_DEFAULT_STATE +#define DIGITAL7_DEFAULT_STATE 1 +#endif + +#ifndef DIGITAL8_PIN +#define DIGITAL8_PIN 2 +#endif + +#ifndef DIGITAL8_PIN_MODE +#define DIGITAL8_PIN_MODE INPUT_PULLUP +#endif + +#ifndef DIGITAL8_DEFAULT_STATE +#define DIGITAL8_DEFAULT_STATE 1 #endif //------------------------------------------------------------------------------ @@ -366,24 +462,173 @@ #define EVENTS_SUPPORT 0 // Do not build with counter support by default #endif -#ifndef EVENTS_TRIGGER -#define EVENTS_TRIGGER 1 // 1 to trigger callback on events, +#ifndef EVENTS1_TRIGGER +#define EVENTS1_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS1_PIN +#define EVENTS1_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS1_PIN_MODE +#define EVENTS1_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS1_INTERRUPT_MODE +#define EVENTS1_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS1_DEBOUNCE +#define EVENTS1_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS2_TRIGGER +#define EVENTS2_TRIGGER 1 // 1 to trigger callback on events, // 0 to only count them and report periodically #endif -#ifndef EVENTS_PIN -#define EVENTS_PIN 2 // GPIO to monitor +#ifndef EVENTS2_PIN +#define EVENTS2_PIN 2 // GPIO to monitor #endif -#ifndef EVENTS_PIN_MODE -#define EVENTS_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#ifndef EVENTS2_PIN_MODE +#define EVENTS2_PIN_MODE INPUT // INPUT, INPUT_PULLUP #endif -#ifndef EVENTS_INTERRUPT_MODE -#define EVENTS_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#ifndef EVENTS2_INTERRUPT_MODE +#define EVENTS2_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE #endif -#define EVENTS_DEBOUNCE 50 // Do not register events within less than 50 millis +#ifndef EVENTS2_DEBOUNCE +#define EVENTS2_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS3_TRIGGER +#define EVENTS3_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS3_PIN +#define EVENTS3_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS3_PIN_MODE +#define EVENTS3_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS3_INTERRUPT_MODE +#define EVENTS3_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS3_DEBOUNCE +#define EVENTS3_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS4_TRIGGER +#define EVENTS4_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS4_PIN +#define EVENTS4_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS4_PIN_MODE +#define EVENTS4_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS4_INTERRUPT_MODE +#define EVENTS4_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS4_DEBOUNCE +#define EVENTS4_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS5_TRIGGER +#define EVENTS5_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS5_PIN +#define EVENTS5_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS5_PIN_MODE +#define EVENTS5_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS5_INTERRUPT_MODE +#define EVENTS5_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS5_DEBOUNCE +#define EVENTS5_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS6_TRIGGER +#define EVENTS6_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS6_PIN +#define EVENTS6_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS6_PIN_MODE +#define EVENTS6_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS6_INTERRUPT_MODE +#define EVENTS6_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS6_DEBOUNCE +#define EVENTS6_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS7_TRIGGER +#define EVENTS7_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS7_PIN +#define EVENTS7_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS7_PIN_MODE +#define EVENTS7_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS7_INTERRUPT_MODE +#define EVENTS7_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS7_DEBOUNCE +#define EVENTS7_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif + +#ifndef EVENTS8_TRIGGER +#define EVENTS8_TRIGGER 1 // 1 to trigger callback on events, + // 0 to only count them and report periodically +#endif + +#ifndef EVENTS8_PIN +#define EVENTS8_PIN 2 // GPIO to monitor +#endif + +#ifndef EVENTS8_PIN_MODE +#define EVENTS8_PIN_MODE INPUT // INPUT, INPUT_PULLUP +#endif + +#ifndef EVENTS8_INTERRUPT_MODE +#define EVENTS8_INTERRUPT_MODE RISING // RISING, FALLING, CHANGE +#endif + +#ifndef EVENTS8_DEBOUNCE +#define EVENTS8_DEBOUNCE 50 // Do not register events within less than 50 millis +#endif //------------------------------------------------------------------------------ // Geiger sensor @@ -669,7 +914,6 @@ #ifndef PULSEMETER_SUPPORT #define PULSEMETER_SUPPORT 0 #endif - #ifndef PULSEMETER_PIN #define PULSEMETER_PIN 5 #endif @@ -1011,6 +1255,19 @@ #define I2C_CLEAR_BUS 0 // Clear I2C bus on boot #define I2C_PERFORM_SCAN 1 // Perform a bus scan on boot +// ----------------------------------------------------------------------------- +// ADE7953 Shelly Sensor +// Enable support by passing ADE7953_SUPPORT=1 build flag +// ----------------------------------------------------------------------------- + +#ifndef ADE7953_SUPPORT +#define ADE7953_SUPPORT 0 +#endif + +#ifndef ADE7953_ADDRESS +#define ADE7953_ADDRESS 0x38 +#endif + //-------------------------------------------------------------------------------- // Class loading //-------------------------------------------------------------------------------- @@ -1161,4 +1418,8 @@ #include "../sensors/VL53L1XSensor.h" #endif +#if ADE7953_SUPPORT + #include "../sensors/ADE7953Sensor.h" +#endif + #endif // SENSOR_SUPPORT diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 62381260..7be27baf 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -75,6 +75,8 @@ #define RELAY_BOOT_ON 1 #define RELAY_BOOT_SAME 2 #define RELAY_BOOT_TOGGLE 3 +#define RELAY_BOOT_LOCKED_OFF 4 +#define RELAY_BOOT_LOCKED_ON 5 #define RELAY_TYPE_NORMAL 0 #define RELAY_TYPE_INVERSE 1 @@ -101,6 +103,10 @@ #define RELAY_GROUP_SYNC_INVERSE 1 #define RELAY_GROUP_SYNC_RECEIVEONLY 2 +#define RELAY_LOCK_OFF 0 +#define RELAY_LOCK_ON 1 +#define RELAY_LOCK_DISABLED 2 + //------------------------------------------------------------------------------ // UDP SYSLOG //------------------------------------------------------------------------------ @@ -146,6 +152,12 @@ #define MQTT_DISCONNECT_EVENT 1 #define MQTT_MESSAGE_EVENT 2 +// MQTT_LIBRARY +#define MQTT_LIBRARY_ASYNCMQTTCLIENT 0 +#define MQTT_LIBRARY_ARDUINOMQTT 1 +#define MQTT_LIBRARY_PUBSUBCLIENT 2 + + //------------------------------------------------------------------------------ // LED //------------------------------------------------------------------------------ @@ -303,6 +315,7 @@ #define SENSOR_BMP180_ID 34 #define SENSOR_MAX6675_ID 35 #define SENSOR_LDR_ID 36 +#define SENSOR_ADE7953_ID 37 //-------------------------------------------------------------------------------- // Magnitudes @@ -343,3 +356,31 @@ #define MAGNITUDE_PH 31 #define MAGNITUDE_MAX 32 + +//------------------------------------------------------------------------------ +// Telnet server +//------------------------------------------------------------------------------ + +#define TELNET_SERVER_ASYNC 0 +#define TELNET_SERVER_WIFISERVER 1 + +//------------------------------------------------------------------------------ +// OTA Client (not related to the Web OTA support) +//------------------------------------------------------------------------------ + +#define OTA_CLIENT_NONE 0 +#define OTA_CLIENT_ASYNCTCP 1 +#define OTA_CLIENT_HTTPUPDATE 2 + +//------------------------------------------------------------------------------ +// Secure Client +//------------------------------------------------------------------------------ + +#define SECURE_CLIENT_NONE 0 +#define SECURE_CLIENT_AXTLS 1 +#define SECURE_CLIENT_BEARSSL 2 + +#define SECURE_CLIENT_CHECK_NONE 0 // !!! INSECURE CONNECTION !!! +#define SECURE_CLIENT_CHECK_FINGERPRINT 1 // legacy fingerprint validation +#define SECURE_CLIENT_CHECK_CA 2 // set trust anchor from PROGMEM CA certificate + diff --git a/code/espurna/crash.ino b/code/espurna/crash.ino index 3a440171..d904b66f 100644 --- a/code/espurna/crash.ino +++ b/code/espurna/crash.ino @@ -29,7 +29,8 @@ extern "C" { * 8. depc * 9. adress of stack start * 10. adress of stack end - * 11. stack trace bytes + * 11. stack trace size + * 12. stack trace bytes * ... */ #define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes @@ -42,12 +43,19 @@ extern "C" { #define SAVE_CRASH_DEPC 0x16 // 4 bytes #define SAVE_CRASH_STACK_START 0x1A // 4 bytes #define SAVE_CRASH_STACK_END 0x1E // 4 bytes -#define SAVE_CRASH_STACK_TRACE 0x22 // variable +#define SAVE_CRASH_STACK_SIZE 0x22 // 2 bytes +#define SAVE_CRASH_STACK_TRACE 0x24 // variable + +#define SAVE_CRASH_STACK_TRACE_MAX 0x80 // limit at 128 bytes (increment/decrement by 16) + +uint16_t _save_crash_stack_trace_max = SAVE_CRASH_STACK_TRACE_MAX; +bool _save_crash_enabled = true; /** * Save crash information in EEPROM * This function is called automatically if ESP8266 suffers an exception * It should be kept quick / consise to be able to execute before hardware wdt may kick in + * This method assumes EEPROM has already been initialized, which is the first thing ESPurna does */ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end ) { @@ -56,37 +64,44 @@ extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack return; } - // This method assumes EEPROM has already been initialized - // which is the first thing ESPurna does + // Check if runtime setting disabled this callback + if (!_save_crash_enabled) { + return; + } - // write crash time to EEPROM + // write crash time to EEPROM, which we will later use as a marker that there was a crash uint32_t crash_time = millis(); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time); - // write reset info to EEPROM + // rst_info::reason and ::exccause are uint32_t, but are holding small values EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason); EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause); - // write epc1, epc2, epc3, excvaddr and depc to EEPROM + // write epc1, epc2, epc3, excvaddr and depc to EEPROM as uint32_t EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc); - // write stack start and end address to EEPROM + // EEPROM size is limited, write as little as possible. + // we sometimes want to avoid big stack traces, e.g. if stack_end == 0x3fffffb0, we are in SYS context. + // but still should get enough relevant info and it is possible to set needed size at build/runtime + const uint16_t stack_size = constrain((stack_end - stack_start), 0, _save_crash_stack_trace_max); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_SIZE, stack_size); - // starting address of Embedis data plus reserve - const uint16_t settings_start = SPI_FLASH_SEC_SIZE - settingsSize() - 0x10; + // starting EEPROM address of Embedis data plus reserve + const uint16_t settings_start = ( + ((SPI_FLASH_SEC_SIZE - settingsSize() + 31) & -32) - 0x20); // write stack trace to EEPROM and avoid overwriting settings - int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; - for (uint32_t i = stack_start; i < stack_end; i++) { - if (current_address >= settings_start) break; - byte* byteValue = (byte*) i; - EEPROMr.write(current_address++, *byteValue); + int16_t eeprom_addr = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; + for (uint32_t* addr = (uint32_t*)stack_start; addr < (uint32_t*)(stack_start + stack_size); addr++) { + if (eeprom_addr >= settings_start) break; + EEPROMr.put(eeprom_addr, *addr); + eeprom_addr += sizeof(uint32_t); } EEPROMr.commit(); @@ -129,19 +144,22 @@ void crashDump() { DEBUG_MSG_P(PSTR("[DEBUG] excvaddr=0x%08x depc=0x%08x\n"), excvaddr, depc); uint32_t stack_start, stack_end; + uint16_t stack_size; + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start); EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end); + EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_SIZE, stack_size); - DEBUG_MSG_P(PSTR("[DEBUG] sp=0x%08x end=0x%08x\n"), stack_start, stack_end); + DEBUG_MSG_P(PSTR("sp=0x%08x end=0x%08x saved=0x%04x\n\n"), stack_start, stack_end, stack_size); + if (0xFFFF == stack_size) return; int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE; - int16_t stack_len = stack_end - stack_start; uint32_t stack_trace; DEBUG_MSG_P(PSTR("[DEBUG] >>>stack>>>\n[DEBUG] ")); - for (int16_t i = 0; i < stack_len; i += 0x10) { + for (int16_t i = 0; i < stack_size; i += 0x10) { DEBUG_MSG_P(PSTR("%08x: "), stack_start + i); for (byte j = 0; j < 4; j++) { EEPROMr.get(current_address, stack_trace); @@ -154,4 +172,23 @@ void crashDump() { } +void crashSetup() { + + #if TERMINAL_SUPPORT + terminalRegisterCommand(F("CRASH"), [](Embedis* e) { + crashDump(); + crashClear(); + terminalOK(); + }); + #endif + + // Minumum of 16 and align for column formatter in crashDump() + _save_crash_stack_trace_max = getSetting("sysTraceMax", SAVE_CRASH_STACK_TRACE_MAX).toInt(); + _save_crash_stack_trace_max = (_save_crash_stack_trace_max + 15) & -16; + setSetting("sysScTraceMax", _save_crash_stack_trace_max); + + _save_crash_enabled = getSetting("sysCrashSave", 1).toInt() == 1; + +} + #endif // DEBUG_SUPPORT diff --git a/code/espurna/debug.ino b/code/espurna/debug.ino index ebe7a0a0..a1f0fe13 100644 --- a/code/espurna/debug.ino +++ b/code/espurna/debug.ino @@ -8,6 +8,8 @@ Copyright (C) 2016-2019 by Xose Pérez #if DEBUG_SUPPORT +#include "libs/DebugSend.h" + #if DEBUG_UDP_SUPPORT #include WiFiUDP _udp_debug; @@ -36,7 +38,7 @@ char _udp_syslog_header[40] = {0}; } #endif -void _debugSend(const char * message) { +void debugSendImpl(const char * message) { const size_t msg_len = strlen(message); @@ -85,63 +87,29 @@ void _debugSend(const char * message) { } -// ----------------------------------------------------------------------------- - -void debugSend(const char * format, ...) { - - va_list args; - va_start(args, format); - char test[1]; - int len = ets_vsnprintf(test, 1, format, args) + 1; - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); - va_end(args); - - _debugSend(buffer); - - delete[] buffer; - -} - -void debugSend_P(PGM_P format_P, ...) { - - char format[strlen_P(format_P)+1]; - memcpy_P(format, format_P, sizeof(format)); - - va_list args; - va_start(args, format_P); - char test[1]; - int len = ets_vsnprintf(test, 1, format, args) + 1; - char * buffer = new char[len]; - ets_vsnprintf(buffer, len, format, args); - va_end(args); - - _debugSend(buffer); - - delete[] buffer; - -} #if DEBUG_WEB_SUPPORT -void debugWebSetup() { - - wsOnSendRegister([](JsonObject& root) { - root["dbgVisible"] = 1; - }); - - wsOnActionRegister([](uint32_t client_id, const char * action, JsonObject& data) { +void _debugWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { #if TERMINAL_SUPPORT if (strcmp(action, "dbgcmd") == 0) { - const char* command = data.get("command"); - char buffer[strlen(command) + 2]; - snprintf(buffer, sizeof(buffer), "%s\n", command); - terminalInject((void*) buffer, strlen(buffer)); + if (!data.containsKey("command") || !data["command"].is()) return; + const char* command = data["command"]; + if (command && strlen(command)) { + auto command = data.get("command"); + terminalInject((void*) command, strlen(command)); + terminalInject('\n'); + } } #endif - - }); +} + +void debugWebSetup() { + + wsRegister() + .onVisible([](JsonObject& root) { root["dbgVisible"] = 1; }) + .onAction(_debugWebSocketOnAction); #if DEBUG_UDP_SUPPORT #if DEBUG_UDP_PORT == 514 diff --git a/code/espurna/domoticz.ino b/code/espurna/domoticz.ino index 7116791e..02b6588c 100644 --- a/code/espurna/domoticz.ino +++ b/code/espurna/domoticz.ino @@ -48,6 +48,8 @@ void _domoticzStatus(unsigned char id, bool status) { #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE +#include "light.h" + void _domoticzLight(unsigned int idx, const JsonObject& root) { if (!lightHasColor()) return; @@ -55,10 +57,34 @@ void _domoticzLight(unsigned int idx, const JsonObject& root) { JsonObject& color = root["Color"]; if (!color.success()) return; + // for ColorMode... see: + // https://github.com/domoticz/domoticz/blob/development/hardware/ColorSwitch.h + // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Set_a_light_to_a_certain_color_or_color_temperature + + DEBUG_MSG_P(PSTR("[DOMOTICZ] Received rgb:%u,%u,%u ww:%u,cw:%u t:%u brightness:%u for IDX %u\n"), + color["r"].as(), + color["g"].as(), + color["b"].as(), + color["ww"].as(), + color["cw"].as(), + color["t"].as(), + color["Level"].as(), + idx + ); + // m field contains information about color mode (enum ColorMode from domoticz ColorSwitch.h): unsigned int cmode = color["m"]; - if (cmode == 3 || cmode == 4) { // ColorModeRGB or ColorModeCustom - see domoticz ColorSwitch.h + if (cmode == 2) { // ColorModeWhite - WW,CW,temperature (t unused for now) + + if (lightChannels() < 2) return; + + lightChannel(0, color["ww"]); + lightChannel(1, color["cw"]); + + } else if (cmode == 3 || cmode == 4) { // ColorModeRGB or ColorModeCustom + + if (lightChannels() < 3) return; lightChannel(0, color["r"]); lightChannel(1, color["g"]); @@ -69,34 +95,21 @@ void _domoticzLight(unsigned int idx, const JsonObject& root) { if (lightChannels() > 3) { lightChannel(3, color["ww"]); } - if (lightChannels() > 4) { lightChannel(4, color["cw"]); } - // domoticz uses 100 as maximum value while we're using LIGHT_MAX_BRIGHTNESS - unsigned int brightness = (root["Level"].as() / 100.0) * LIGHT_MAX_BRIGHTNESS; - lightBrightness(brightness); - - DEBUG_MSG_P(PSTR("[DOMOTICZ] Received rgb:%u,%u,%u ww:%u,cw:%u brightness:%u for IDX %u\n"), - color["r"].as(), - color["g"].as(), - color["b"].as(), - color["ww"].as(), - color["cw"].as(), - brightness, - idx - ); - - lightUpdate(true, mqttForward()); - } + // domoticz uses 100 as maximum value while we're using Light::BRIGHTNESS_MAX (unsigned char) + lightBrightness((root["Level"].as() / 100.0) * Light::BRIGHTNESS_MAX); + lightUpdate(true, mqttForward()); + } #endif -void _domoticzMqtt(unsigned int type, const char * topic, const char * payload) { +void _domoticzMqtt(unsigned int type, const char * topic, char * payload) { if (!_dcz_enabled) return; @@ -118,8 +131,8 @@ void _domoticzMqtt(unsigned int type, const char * topic, const char * payload) if (dczTopicOut.equals(topic)) { // Parse response - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.parseObject((char *) payload); + DynamicJsonBuffer jsonBuffer(1024); + JsonObject& root = jsonBuffer.parseObject(payload); if (!root.success()) { DEBUG_MSG_P(PSTR("[DOMOTICZ] Error parsing data\n")); return; @@ -166,13 +179,16 @@ void _domoticzBrokerCallback(const unsigned char type, const char * topic, unsig #if WEB_SUPPORT -bool _domoticzWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _domoticzWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "dcz", 3) == 0); } -void _domoticzWebSocketOnSend(JsonObject& root) { +void _domoticzWebSocketOnVisible(JsonObject& root) { + root["dczVisible"] = static_cast(haveRelaysOrSensors()); +} + +void _domoticzWebSocketOnConnected(JsonObject& root) { - unsigned char visible = 0; root["dczEnabled"] = getSetting("dczEnabled", DOMOTICZ_ENABLED).toInt() == 1; root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC); root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC); @@ -181,15 +197,11 @@ void _domoticzWebSocketOnSend(JsonObject& root) { for (unsigned char i=0; i 0); #if SENSOR_SUPPORT _sensorWebSocketMagnitudes(root, "dcz"); - visible = visible || (magnitudeCount() > 0); #endif - root["dczVisible"] = visible; - } #endif // WEB_SUPPORT @@ -248,8 +260,10 @@ void domoticzSetup() { _domoticzConfigure(); #if WEB_SUPPORT - wsOnSendRegister(_domoticzWebSocketOnSend); - wsOnReceiveRegister(_domoticzWebSocketOnReceive); + wsRegister() + .onVisible(_domoticzWebSocketOnVisible) + .onConnected(_domoticzWebSocketOnConnected) + .onKeyCheck(_domoticzWebSocketOnKeyCheck); #endif #if BROKER_SUPPORT diff --git a/code/espurna/eeprom.ino b/code/espurna/eeprom.ino index 2c2e5da9..9b03f80e 100644 --- a/code/espurna/eeprom.ino +++ b/code/espurna/eeprom.ino @@ -23,6 +23,9 @@ void eepromRotate(bool value) { DEBUG_MSG_P(PSTR("[EEPROM] Disabling EEPROM rotation\n")); } EEPROMr.rotate(value); + + // Because .rotate(false) marks EEPROM as dirty, this is equivalent to the .backup(0) + eepromCommit(); } } @@ -54,6 +57,10 @@ void eepromCommit() { _eeprom_commit = true; } +void eepromBackup(uint32_t index){ + EEPROMr.backup(index); +} + #if TERMINAL_SUPPORT void _eepromInitCommands() { diff --git a/code/espurna/encoder.ino b/code/espurna/encoder.ino index 1836e91c..4d777524 100644 --- a/code/espurna/encoder.ino +++ b/code/espurna/encoder.ino @@ -8,7 +8,7 @@ Copyright (C) 2018-2019 by Xose Pérez #if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) -#include +#include "libs/Encoder.h" #include typedef struct { @@ -22,6 +22,7 @@ typedef struct { } encoder_t; std::vector _encoders; +unsigned long _encoder_min_delta = 1; void _encoderConfigure() { @@ -85,24 +86,22 @@ void _encoderConfigure() { } } + _encoder_min_delta = getSetting("encMinDelta", ENCODER_MINIMUM_DELTA).toInt(); + if (!_encoder_min_delta) _encoder_min_delta = 1; + } void _encoderLoop() { - // for each encoder + // for each encoder, read delta (read()) and map button action for (unsigned char i=0; i<_encoders.size(); i++) { - // get encoder encoder_t encoder = _encoders[i]; - // read encoder long delta = encoder.encoder->read(); encoder.encoder->write(0); - if (0 == delta) continue; - - DEBUG_MSG_P(PSTR("[ENCODER] Delta: %d\n"), delta); + if ((0 == delta) || (_encoder_min_delta > abs(delta))) continue; - // action if (encoder.button_pin == GPIO_NONE) { // if there is no button, the encoder drives CHANNEL1 @@ -110,7 +109,7 @@ void _encoderLoop() { } else { - // check if button is pressed + // otherwise, use button based on encoder mode bool pressed = (digitalRead(encoder.button_pin) != encoder.button_logic); if (ENCODER_MODE_CHANNEL == encoder.mode) { diff --git a/code/espurna/espurna.ino b/code/espurna/espurna.ino index e4322f36..583b24c7 100644 --- a/code/espurna/espurna.ino +++ b/code/espurna/espurna.ino @@ -22,9 +22,12 @@ along with this program. If not, see . #include "config/all.h" #include +#include "libs/HeapStats.h" + std::vector _loop_callbacks; std::vector _reload_callbacks; +bool _reload_config = false; unsigned long _loop_delay = 0; // ----------------------------------------------------------------------------- @@ -40,6 +43,10 @@ void espurnaRegisterReload(void (*callback)()) { } void espurnaReload() { + _reload_config = true; +} + +void _espurnaReload() { for (unsigned char i = 0; i < _reload_callbacks.size(); i++) { (_reload_callbacks[i])(); } @@ -60,7 +67,7 @@ void setup() { // ------------------------------------------------------------------------- // Cache initial free heap value - getInitialFreeHeap(); + setInitialFreeHeap(); // Serial debug #if DEBUG_SUPPORT @@ -76,6 +83,15 @@ void setup() { // Init persistance settingsSetup(); + // Init crash recorder + #if DEBUG_SUPPORT + crashSetup(); + #endif + + // Return bogus free heap value for broken devices + // XXX: device is likely to trigger other bugs! tread carefuly + wtfHeap(getSetting("wtfHeap", 0).toInt()); + // Init Serial, SPIFFS and system check systemSetup(); @@ -94,10 +110,15 @@ void setup() { info(); wifiSetup(); - otaSetup(); + #if OTA_ARDUINOOTA_SUPPORT + arduinoOtaSetup(); + #endif #if TELNET_SUPPORT telnetSetup(); #endif + #if OTA_CLIENT != OTA_CLIENT_NONE + otaClientSetup(); + #endif // ------------------------------------------------------------------------- // Check if system is stable @@ -171,6 +192,9 @@ void setup() { #if NOFUSS_SUPPORT nofussSetup(); #endif + #if SENSOR_SUPPORT + sensorSetup(); + #endif #if INFLUXDB_SUPPORT idbSetup(); #endif @@ -189,9 +213,6 @@ void setup() { #if HOMEASSISTANT_SUPPORT haSetup(); #endif - #if SENSOR_SUPPORT - sensorSetup(); - #endif #if SCHEDULER_SUPPORT schSetup(); #endif @@ -231,6 +252,12 @@ void setup() { void loop() { + // Reload config before running any callbacks + if (_reload_config) { + _espurnaReload(); + _reload_config = false; + } + // Call registered loop callbacks for (unsigned char i = 0; i < _loop_callbacks.size(); i++) { (_loop_callbacks[i])(); diff --git a/code/espurna/homeassistant.ino b/code/espurna/homeassistant.ino index 079e58d9..6a56f0fa 100644 --- a/code/espurna/homeassistant.ino +++ b/code/espurna/homeassistant.ino @@ -9,7 +9,6 @@ Copyright (C) 2017-2019 by Xose Pérez #if HOMEASSISTANT_SUPPORT #include -#include bool _haEnabled = false; bool _haSendFlag = false; @@ -18,13 +17,74 @@ bool _haSendFlag = false; // UTILS // ----------------------------------------------------------------------------- -String _haFixName(String name) { +// per yaml 1.1 spec, following scalars are converted to bool. we want the string, so quoting the output +// y|Y|yes|Yes|YES|n|N|no|No|NO |true|True|TRUE|false|False|FALSE |on|On|ON|off|Off|OFF +String _haFixPayload(const String& value) { + if (value.equalsIgnoreCase("y") + || value.equalsIgnoreCase("n") + || value.equalsIgnoreCase("yes") + || value.equalsIgnoreCase("no") + || value.equalsIgnoreCase("true") + || value.equalsIgnoreCase("false") + || value.equalsIgnoreCase("on") + || value.equalsIgnoreCase("off") + ) { + String temp; + temp.reserve(value.length() + 2); + temp = "\""; + temp += value; + temp += "\""; + return temp; + } + return value; +} + +String& _haFixName(String& name) { for (unsigned char i=0; i 3) { @@ -118,145 +178,113 @@ void _haSendSwitch(unsigned char i, JsonObject& config) { } -void _haSendSwitches(const JsonObject& deviceConfig) { - - #if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER)) - String type = String("light"); - #else - String type = String("switch"); - #endif +void _haSendSwitches(ha_config_t& config) { for (unsigned char i=0; i printer, bool wrapJson = false) { +constexpr const size_t HA_YAML_BUFFER_SIZE = 1024; - #if (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE) || (defined(ITEAD_SLAMPHER)) - String type = String("light"); - #else - String type = String("switch"); - #endif +void _haSwitchYaml(unsigned char index, JsonObject& root) { - for (unsigned char i=0; i()); + } else { output += kv.value.as(); - output += "\n"; - } - output += " "; - - if (wrapJson) { - output += "\"}"; } - - jsonBuffer.clear(); - - printer(output); - + output += "\n"; } + output += " "; - #if SENSOR_SUPPORT - - for (unsigned char i=0; i(); - value.replace("%", "'%'"); - output += kv.key; - output += ": "; - output += value; - output += "\n"; - } - output += " "; +void _haSensorYaml(unsigned char index, JsonObject& root) { - if (wrapJson) { - output += "\"}"; - } + String output; + output.reserve(HA_YAML_BUFFER_SIZE); - jsonBuffer.clear(); + JsonObject& config = root.createNestedObject("config"); + _haSendMagnitude(index, config); - printer(output); + if (index == 0) output += "\n\nsensor:"; + output += "\n"; + bool first = true; + for (auto kv : config) { + if (first) { + output += " - "; + first = false; + } else { + output += " "; } + String value = kv.value.as(); + value.replace("%", "'%'"); + output += kv.key; + output += ": "; + output += value; + output += "\n"; + } + output += " "; + + root.remove("config"); + root["haConfig"] = output; - #endif } +#endif // SENSOR_SUPPORT + void _haGetDeviceConfig(JsonObject& config) { - String identifier = getIdentifier(); - - config.createNestedArray("identifiers").add(identifier); + config.createNestedArray("identifiers").add(getIdentifier()); config["name"] = getSetting("desc", getSetting("hostname")); - config["manufacturer"] = String(MANUFACTURER); - config["model"] = String(DEVICE); - config["sw_version"] = String(APP_NAME) + " " + String(APP_VERSION) + " (" + getCoreVersion() + ")"; + config["manufacturer"] = MANUFACTURER; + config["model"] = DEVICE; + config["sw_version"] = String(APP_NAME) + " " + APP_VERSION + " (" + getCoreVersion() + ")"; } void _haSend() { @@ -270,17 +298,14 @@ void _haSend() { DEBUG_MSG_P(PSTR("[HA] Sending autodiscovery MQTT message\n")); // Get common device config - DynamicJsonBuffer jsonBuffer; - JsonObject& deviceConfig = jsonBuffer.createObject(); - _haGetDeviceConfig(deviceConfig); + ha_config_t config; // Send messages - _haSendSwitches(deviceConfig); + _haSendSwitches(config); #if SENSOR_SUPPORT - _haSendMagnitudes(deviceConfig); + _haSendMagnitudes(config); #endif - jsonBuffer.clear(); _haSendFlag = false; } @@ -294,34 +319,68 @@ void _haConfigure() { #if WEB_SUPPORT -std::queue _ha_send_config; - -bool _haWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _haWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "ha", 2) == 0); } -void _haWebSocketOnSend(JsonObject& root) { +void _haWebSocketOnVisible(JsonObject& root) { root["haVisible"] = 1; +} + +void _haWebSocketOnConnected(JsonObject& root) { root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX); root["haEnabled"] = getSetting("haEnabled", HOMEASSISTANT_ENABLED).toInt() == 1; } void _haWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { if (strcmp(action, "haconfig") == 0) { - _ha_send_config.push(client_id); + ws_on_send_callback_list_t callbacks; + #if SENSOR_SUPPORT + callbacks.reserve(magnitudeCount() + relayCount()); + #else + callbacks.reserve(relayCount()); + #endif // SENSOR_SUPPORT + { + for (unsigned char idx=0; idx().c_str()); + } + #if SENSOR_SUPPORT + for (unsigned char idx=0; idx().c_str()); + } + #endif // SENSOR_SUPPORT DEBUG_MSG("\n"); terminalOK(); }); @@ -330,7 +389,7 @@ void _haInitCommands() { setSetting("haEnabled", "1"); _haConfigure(); #if WEB_SUPPORT - wsSend(_haWebSocketOnSend); + wsPost(_haWebSocketOnConnected); #endif terminalOK(); }); @@ -339,7 +398,7 @@ void _haInitCommands() { setSetting("haEnabled", "0"); _haConfigure(); #if WEB_SUPPORT - wsSend(_haWebSocketOnSend); + wsPost(_haWebSocketOnConnected); #endif terminalOK(); }); @@ -350,32 +409,16 @@ void _haInitCommands() { // ----------------------------------------------------------------------------- -#if WEB_SUPPORT -void _haLoop() { - if (_ha_send_config.empty()) return; - - uint32_t client_id = _ha_send_config.front(); - _ha_send_config.pop(); - - if (!wsConnected(client_id)) return; - - // TODO check wsConnected after each "printer" call? - _haDumpConfig([client_id](String& output) { - wsSend(client_id, output.c_str()); - yield(); - }, true); -} -#endif - void haSetup() { _haConfigure(); #if WEB_SUPPORT - wsOnSendRegister(_haWebSocketOnSend); - wsOnActionRegister(_haWebSocketOnAction); - wsOnReceiveRegister(_haWebSocketOnReceive); - espurnaRegisterLoop(_haLoop); + wsRegister() + .onVisible(_haWebSocketOnVisible) + .onConnected(_haWebSocketOnConnected) + .onAction(_haWebSocketOnAction) + .onKeyCheck(_haWebSocketOnKeyCheck); #endif #if TERMINAL_SUPPORT @@ -385,6 +428,7 @@ void haSetup() { // On MQTT connect check if we have something to send mqttRegister([](unsigned int type, const char * topic, const char * payload) { if (type == MQTT_CONNECT_EVENT) _haSend(); + if (type == MQTT_DISCONNECT_EVENT) _haSendFlag = false; }); // Main callbacks diff --git a/code/espurna/influxdb.ino b/code/espurna/influxdb.ino index 5a3b1422..0701858c 100644 --- a/code/espurna/influxdb.ino +++ b/code/espurna/influxdb.ino @@ -17,12 +17,15 @@ SyncClientWrap * _idb_client; // ----------------------------------------------------------------------------- -bool _idbWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "idb", 3) == 0); } -void _idbWebSocketOnSend(JsonObject& root) { +void _idbWebSocketOnVisible(JsonObject& root) { root["idbVisible"] = 1; +} + +void _idbWebSocketOnConnected(JsonObject& root) { root["idbEnabled"] = getSetting("idbEnabled", INFLUXDB_ENABLED).toInt() == 1; root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST); root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT).toInt(); @@ -118,8 +121,10 @@ void idbSetup() { _idbConfigure(); #if WEB_SUPPORT - wsOnSendRegister(_idbWebSocketOnSend); - wsOnReceiveRegister(_idbWebSocketOnReceive); + wsRegister() + .onVisible(_idbWebSocketOnVisible) + .onConnected(_idbWebSocketOnConnected) + .onKeyCheck(_idbWebSocketOnKeyCheck); #endif #if BROKER_SUPPORT diff --git a/code/espurna/ir.ino b/code/espurna/ir.ino index cf86d203..780dbe7a 100644 --- a/code/espurna/ir.ino +++ b/code/espurna/ir.ino @@ -303,7 +303,8 @@ void _irProcess(unsigned char type, unsigned long code) { } if (button_mode == IR_BUTTON_MODE_RGB) { - lightColor(button_value); + lightColor((button_value >> 8) & 0xffffff); + lightBrightness(button_value & 0xff); } /* diff --git a/code/espurna/led.ino b/code/espurna/led.ino index 2a882899..036e48ca 100644 --- a/code/espurna/led.ino +++ b/code/espurna/led.ino @@ -71,13 +71,16 @@ void _ledBlink(unsigned char id, unsigned long delayOff, unsigned long delayOn) #if WEB_SUPPORT -bool _ledWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _ledWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "led", 3) == 0); } -void _ledWebSocketOnSend(JsonObject& root) { - if (_ledCount() == 0) return; +void _ledWebSocketOnVisible(JsonObject& root) { root["ledVisible"] = 1; +} + +void _ledWebSocketOnConnected(JsonObject& root) { + if (_ledCount() == 0) return; JsonArray& leds = root.createNestedArray("ledConfig"); for (byte i=0; i<_ledCount(); i++) { JsonObject& led = leds.createNestedObject(); @@ -127,13 +130,13 @@ void _ledMQTTCallback(unsigned int type, const char * topic, const char * payloa if (_ledMode(ledID) != LED_MODE_MQTT) return; // get value - unsigned char value = relayParsePayload(payload); + const auto value = relayParsePayload(payload); // Action to perform - if (value == 2) { + if (value == RelayStatus::TOGGLE) { _ledToggle(ledID); } else { - _ledStatus(ledID, value == 1); + _ledStatus(ledID, (value == RelayStatus::ON)); } } @@ -200,8 +203,10 @@ void ledSetup() { #endif #if WEB_SUPPORT - wsOnSendRegister(_ledWebSocketOnSend); - wsOnReceiveRegister(_ledWebSocketOnReceive); + wsRegister() + .onVisible(_ledWebSocketOnVisible) + .onConnected(_ledWebSocketOnConnected) + .onKeyCheck(_ledWebSocketOnKeyCheck); #endif #if BROKER_SUPPORT diff --git a/code/espurna/libs/DebugSend.h b/code/espurna/libs/DebugSend.h new file mode 100644 index 00000000..11cc5281 --- /dev/null +++ b/code/espurna/libs/DebugSend.h @@ -0,0 +1,47 @@ +// ----------------------------------------------------------------------------- +// printf-like debug methods +// ----------------------------------------------------------------------------- + +#pragma once + +void debugSendImpl(const char*); + +void _debugSend(const char * format, va_list args) { + + char temp[64]; + int len = ets_vsnprintf(temp, sizeof(temp), format, args); + if (len < 64) { debugSendImpl(temp); return; } + + auto buffer = new char[len + 1]; + ets_vsnprintf(buffer, len + 1, format, args); + + debugSendImpl(buffer); + + delete[] buffer; + +} + +void debugSend(const char* format, ...) { + + va_list args; + va_start(args, format); + + _debugSend(format, args); + + va_end(args); + +} + +void debugSend_P(PGM_P format_P, ...) { + + char format[strlen_P(format_P) + 1]; + memcpy_P(format, format_P, sizeof(format)); + + va_list args; + va_start(args, format_P); + + _debugSend(format, args); + + va_end(args); + +} diff --git a/code/espurna/libs/Encoder.h b/code/espurna/libs/Encoder.h new file mode 100644 index 00000000..7e24e1d5 --- /dev/null +++ b/code/espurna/libs/Encoder.h @@ -0,0 +1,228 @@ +/* ---------------------------- Original copyright ----------------------------- + * + * Encoder Library, for measuring quadrature encoded signals + * http://www.pjrc.com/teensy/td_libs_Encoder.html + * Copyright (c) 2011,2013 PJRC.COM, LLC - Paul Stoffregen + * + * Version 1.2 - fix -2 bug in C-only code + * Version 1.1 - expand to support boards with up to 60 interrupts + * Version 1.0 - initial release + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ----------------------------------------------------------------------------- + * + * Encoder.h, updated for ESP8266 use in ESPurna. Other hardware is not supported. + * + * - Added ESP-specific attributes to ISR handlers to place them in IRAM. + * - Reduced per-encoder structure sizes - only 5 Encoders can be used on ESP8266, + * and we can directly reference pin number instead of storing both register and bitmask + * + */ + +#pragma once + +// _______ _______ +// Pin1 ______| |_______| |______ Pin1 +// negative <--- _______ _______ __ --> positive +// Pin2 __| |_______| |_______| Pin2 + +// new new old old +// pin2 pin1 pin2 pin1 Result +// ---- ---- ---- ---- ------ +// 0 0 0 0 no movement +// 0 0 0 1 +1 +// 0 0 1 0 -1 +// 0 0 1 1 +2 (assume pin1 edges only) +// 0 1 0 0 -1 +// 0 1 0 1 no movement +// 0 1 1 0 -2 (assume pin1 edges only) +// 0 1 1 1 +1 +// 1 0 0 0 +1 +// 1 0 0 1 -2 (assume pin1 edges only) +// 1 0 1 0 no movement +// 1 0 1 1 -1 +// 1 1 0 0 +2 (assume pin1 edges only) +// 1 1 0 1 -1 +// 1 1 1 0 +1 +// 1 1 1 1 no movement + +namespace EncoderLibrary { + + typedef struct { + uint8_t pin1; + uint8_t pin2; + uint8_t state; + int32_t position; + } encoder_values_t; + + constexpr const unsigned char ENCODERS_MAXIMUM {5u}; + + encoder_values_t * EncoderValues[ENCODERS_MAXIMUM] = {nullptr}; + + uint8_t _encoderFindStorage() { + for (uint8_t i = 0; i < ENCODERS_MAXIMUM; i++) { + if (EncoderValues[i] == nullptr) { + return i; + } + } + return ENCODERS_MAXIMUM; + } + + void _encoderCleanStorage(uint8_t pin1, uint8_t pin2) { + for (uint8_t i = 0; i < ENCODERS_MAXIMUM; i++) { + if (EncoderValues[i] == nullptr) continue; + if (((EncoderValues[i])->pin1 == pin1) && ((EncoderValues[i])->pin2 == pin2)) { + EncoderValues[i] = nullptr; + break; + } + } + } + + // update() is not meant to be called from outside Encoder, + // but it is public to allow static interrupt routines. + void ICACHE_RAM_ATTR update(encoder_values_t *target) { + uint8_t p1val = GPIP(target->pin1); + uint8_t p2val = GPIP(target->pin2); + uint8_t state = target->state & 3; + if (p1val) state |= 4; + if (p2val) state |= 8; + target->state = (state >> 2); + switch (state) { + case 1: case 7: case 8: case 14: + target->position++; + return; + case 2: case 4: case 11: case 13: + target->position--; + return; + case 3: case 12: + target->position += 2; + return; + case 6: case 9: + target->position -= 2; + return; + } + } + + // 2 pins per encoder, 1 isr per encoder + void ICACHE_RAM_ATTR isr0() { update(EncoderValues[0]); } + void ICACHE_RAM_ATTR isr1() { update(EncoderValues[1]); } + void ICACHE_RAM_ATTR isr2() { update(EncoderValues[2]); } + void ICACHE_RAM_ATTR isr3() { update(EncoderValues[3]); } + void ICACHE_RAM_ATTR isr4() { update(EncoderValues[4]); } + + constexpr void (*_isr_funcs[5])() = { + isr0, isr1, isr2, isr3, isr4 + }; + + class Encoder { + + private: + + encoder_values_t values; + + public: + + Encoder(uint8_t pin1, uint8_t pin2) { + + values.pin1 = pin1; + values.pin2 = pin2; + + pinMode(values.pin1, INPUT_PULLUP); + pinMode(values.pin2, INPUT_PULLUP); + + values.position = 0; + + // allow time for a passive R-C filter to charge + // through the pullup resistors, before reading + // the initial state + delayMicroseconds(2000); + + uint8_t current = 0; + if (GPIP(values.pin1)) { + current |= 1; + } + + if (GPIP(values.pin2)) { + current |= 2; + } + + values.state = current; + + attach(); + + } + + ~Encoder() { + detach(); + } + + uint8_t pin1() { + return values.pin1; + } + + uint8_t pin2() { + return values.pin2; + } + + int32_t read() { + noInterrupts(); + + update(&values); + int32_t ret = values.position; + + interrupts(); + return ret; + } + + void write(int32_t position) { + noInterrupts(); + values.position = position; + interrupts(); + } + + bool attach() { + uint8_t index = _encoderFindStorage(); + if (index >= ENCODERS_MAXIMUM) return false; + + EncoderValues[index] = &values; + + attachInterrupt(values.pin1, _isr_funcs[index], CHANGE); + attachInterrupt(values.pin2, _isr_funcs[index], CHANGE); + + return true; + } + + void detach() { + noInterrupts(); + + _encoderCleanStorage(values.pin1, values.pin2); + + detachInterrupt(values.pin1); + detachInterrupt(values.pin2); + + interrupts(); + } + + + }; + +} + +using EncoderLibrary::Encoder; diff --git a/code/espurna/libs/HeapStats.h b/code/espurna/libs/HeapStats.h new file mode 100644 index 00000000..70f90f9a --- /dev/null +++ b/code/espurna/libs/HeapStats.h @@ -0,0 +1,112 @@ +/* + +Show extended heap stats when EspClass::getHeapStats() is available + +*/ + +#pragma once + +#include + +struct heap_stats_t { + uint32_t available; + uint16_t usable; + uint8_t frag_pct; +}; + +namespace EspClass_has_getHeapStats { + struct _detector { + template().getHeapStats(0,0,0))> + static std::true_type detect(int); + + template + static std::false_type detect(...); + }; + + template + struct detector : public _detector { + using result = decltype( + std::declval().template detect(0)); + }; + + template + struct typed_check : public detector::result { + }; + + typed_check check{}; +}; + +template +void _getHeapStats(std::true_type&, T& instance, heap_stats_t& stats) { + instance.getHeapStats(&stats.available, &stats.usable, &stats.frag_pct); +} + +template +void _getHeapStats(std::false_type&, T& instance, heap_stats_t& stats) { + stats.available = instance.getFreeHeap(); + stats.usable = 0; + stats.frag_pct = 0; +} + +void getHeapStats(heap_stats_t& stats) { + _getHeapStats(EspClass_has_getHeapStats::check, ESP, stats); +} + +// WTF +// Calling ESP.getFreeHeap() is making the system crash on a specific +// AiLight bulb, but anywhere else it should work as expected +static bool _heap_value_wtf = false; + +heap_stats_t getHeapStats() { + heap_stats_t stats; + if (_heap_value_wtf) { + stats.available = 9999; + stats.usable = 9999; + stats.frag_pct = 0; + return stats; + } + getHeapStats(stats); + return stats; +} + +void wtfHeap(bool value) { + _heap_value_wtf = value; +} + +unsigned int getFreeHeap() { + return ESP.getFreeHeap(); +} + +static unsigned int _initial_heap_value = 0; +void setInitialFreeHeap() { + _initial_heap_value = getFreeHeap(); +} + +unsigned int getInitialFreeHeap() { + if (0 == _initial_heap_value) { + setInitialFreeHeap(); + } + return _initial_heap_value; +} + +void infoMemory(const char* name, const heap_stats_t& stats) { + infoMemory(name, getInitialFreeHeap(), stats.available); +} + +void infoHeapStats(const char* name, const heap_stats_t& stats) { + DEBUG_MSG_P( + PSTR("[MAIN] %-6s: %5u contiguous bytes available (%u%% fragmentation)\n"), + name, + stats.usable, + stats.frag_pct + ); +} + +void infoHeapStats(bool show_frag_stats = true) { + const auto stats = getHeapStats(); + infoMemory("Heap", stats); + if (show_frag_stats && EspClass_has_getHeapStats::check) { + infoHeapStats("Heap", stats); + } +} diff --git a/code/espurna/libs/SecureClientHelpers.h b/code/espurna/libs/SecureClientHelpers.h new file mode 100644 index 00000000..0a17e28c --- /dev/null +++ b/code/espurna/libs/SecureClientHelpers.h @@ -0,0 +1,247 @@ +// ----------------------------------------------------------------------------- +// WiFiClientSecure validation helpers +// ----------------------------------------------------------------------------- + +#pragma once + +#if SECURE_CLIENT != SECURE_CLIENT_NONE + +#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL +#include +#elif SECURE_CLIENT == SECURE_CLIENT_AXTLS +#include +#endif + +namespace SecureClientHelpers { + +using host_callback_f = std::function; +using check_callback_f = std::function; +using fp_callback_f = std::function; +using cert_callback_f = std::function; +using mfln_callback_f = std::function; + +const char * _secureClientCheckAsString(int check) { + switch (check) { + case SECURE_CLIENT_CHECK_NONE: return "no validation"; + case SECURE_CLIENT_CHECK_FINGERPRINT: return "fingerprint validation"; + case SECURE_CLIENT_CHECK_CA: return "CA validation"; + default: return "unknown"; + } +} + +#if SECURE_CLIENT == SECURE_CLIENT_AXTLS +using SecureClientClass = axTLS::WiFiClientSecure; + +struct SecureClientConfig { + SecureClientConfig(const char* tag, host_callback_f host_cb, check_callback_f check_cb, fp_callback_f fp_cb, bool debug = false) : + tag(tag), + on_host(host_cb), + on_check(check_cb), + on_fingerprint(fp_cb), + debug(debug) + {} + + String tag; + host_callback_f on_host; + check_callback_f on_check; + fp_callback_f on_fingerprint; + bool debug; +}; + +struct SecureClientChecks { + + SecureClientChecks(SecureClientConfig& config) : + config(config) + {} + + int getCheck() { + return (config.on_check) ? config.on_check() : (SECURE_CLIENT_CHECK); + } + + bool beforeConnected(SecureClientClass& client) { + return true; + } + + // Special condition for legacy client! + // Otherwise, we are required to connect twice. And it is deemed broken & deprecated anyways... + bool afterConnected(SecureClientClass& client) { + bool result = false; + + int check = getCheck(); + + if(config.debug) { + DEBUG_MSG_P(PSTR("[%s] Using SSL check type: %s\n"), config.tag.c_str(), _secureClientCheckAsString(check)); + } + + if (check == SECURE_CLIENT_CHECK_NONE) { + if (config.debug) DEBUG_MSG_P(PSTR("[%s] !!! Secure connection will not be validated !!!\n"), config.tag.c_str()); + result = true; + } else if (check == SECURE_CLIENT_CHECK_FINGERPRINT) { + if (config.on_fingerprint) { + char _buffer[60] = {0}; + if (config.on_fingerprint && config.on_host && sslFingerPrintChar(config.on_fingerprint().c_str(), _buffer)) { + result = client.verify(_buffer, config.on_host().c_str()); + } + if (!result) DEBUG_MSG_P(PSTR("[%s] Wrong fingerprint, cannot connect\n"), config.tag.c_str()); + } + } else if (check == SECURE_CLIENT_CHECK_CA) { + if (config.debug) DEBUG_MSG_P(PSTR("[%s] CA verification is not supported with axTLS client\n"), config.tag.c_str()); + } + + return result; + } + + SecureClientConfig& config; + bool debug; + +}; +#endif // SECURE_CLIENT_AXTLS + +#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL + +using SecureClientClass = BearSSL::WiFiClientSecure; + +struct SecureClientConfig { + SecureClientConfig(const char* tag, check_callback_f check_cb, cert_callback_f cert_cb, fp_callback_f fp_cb, mfln_callback_f mfln_cb, bool debug = false) : + tag(tag), + on_check(check_cb), + on_certificate(cert_cb), + on_fingerprint(fp_cb), + on_mfln(mfln_cb), + debug(debug) + {} + + String tag; + check_callback_f on_check; + cert_callback_f on_certificate; + fp_callback_f on_fingerprint; + mfln_callback_f on_mfln; + bool debug; +}; + +struct SecureClientChecks { + + SecureClientChecks(SecureClientConfig& config) : + config(config) + {} + + int getCheck() { + return (config.on_check) ? config.on_check() : (SECURE_CLIENT_CHECK); + } + + bool prepareMFLN(SecureClientClass& client) { + const uint16_t requested_mfln = (config.on_mfln) ? config.on_mfln() : (SECURE_CLIENT_MFLN); + bool result = false; + switch (requested_mfln) { + // default, do nothing + case 0: + result = true; + break; + // match valid sizes only + case 512: + case 1024: + case 2048: + case 4096: + { + client.setBufferSizes(requested_mfln, requested_mfln); + result = true; + if (config.debug) { + DEBUG_MSG_P(PSTR("[%s] MFLN buffer size set to %u\n"), config.tag.c_str(), requested_mfln); + } + break; + } + default: + { + if (config.debug) { + DEBUG_MSG_P(PSTR("[%s] Warning: MFLN buffer size must be one of 512, 1024, 2048 or 4096\n"), config.tag.c_str()); + } + } + } + + return result; + } + + bool beforeConnected(SecureClientClass& client) { + int check = getCheck(); + bool settime = (check == SECURE_CLIENT_CHECK_CA); + + if(config.debug) { + DEBUG_MSG_P(PSTR("[%s] Using SSL check type: %s\n"), config.tag.c_str(), _secureClientCheckAsString(check)); + } + + if (!ntpSynced() && settime) { + if (config.debug) DEBUG_MSG_P(PSTR("[%s] Time not synced! Cannot use CA validation\n"), config.tag.c_str()); + return false; + } + + prepareMFLN(client); + + if (check == SECURE_CLIENT_CHECK_NONE) { + if (config.debug) DEBUG_MSG_P(PSTR("[%s] !!! Secure connection will not be validated !!!\n"), config.tag.c_str()); + client.setInsecure(); + } else if (check == SECURE_CLIENT_CHECK_FINGERPRINT) { + uint8_t _buffer[20] = {0}; + if (config.on_fingerprint && sslFingerPrintArray(config.on_fingerprint().c_str(), _buffer)) { + client.setFingerprint(_buffer); + } + } else if (check == SECURE_CLIENT_CHECK_CA) { + client.setX509Time(ntpLocal2UTC(now())); + if (!certs.getCount()) { + if (config.on_certificate) certs.append(config.on_certificate()); + } + client.setTrustAnchors(&certs); + } + + return true; + } + + bool afterConnected(SecureClientClass&) { + return true; + } + + bool debug; + + SecureClientConfig& config; + + BearSSL::X509List certs; + +}; +#endif // SECURE_CLIENT_BEARSSL + +class SecureClient { + + public: + + SecureClient(SecureClientConfig& config) : + _config(config), + _checks(_config), + _client(std::make_unique()) + {} + + bool afterConnected() { + return _checks.afterConnected(get()); + } + + bool beforeConnected() { + return _checks.beforeConnected(get()); + } + + SecureClientClass& get() { + return *_client.get(); + } + + private: + + SecureClientConfig _config; + SecureClientChecks _checks; + std::unique_ptr _client; + +}; + +}; + +using SecureClientConfig = SecureClientHelpers::SecureClientConfig; +using SecureClientChecks = SecureClientHelpers::SecureClientChecks; +using SecureClient = SecureClientHelpers::SecureClient; + +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE diff --git a/code/espurna/libs/URL.h b/code/espurna/libs/URL.h new file mode 100644 index 00000000..a51e54fd --- /dev/null +++ b/code/espurna/libs/URL.h @@ -0,0 +1,68 @@ +// ----------------------------------------------------------------------------- +// Parse char string as URL +// +// Adapted from HTTPClient::beginInternal() +// https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.cpp +// +// ----------------------------------------------------------------------------- + +#pragma once + +struct URL { + String value; + String protocol; + String host; + String path; + uint16_t port; + + URL(const char* url) { init(url); } + URL(const String& url) { init(url); } + + void init(String url); +}; + +void URL::init(String url) { + + this->value = url; + + // cut the protocol part + int index = url.indexOf("://"); + if (index > 0) { + this->protocol = url.substring(0, index); + url.remove(0, (index + 3)); + } + + if (this->protocol == "http") { + this->port = 80; + } else if (this->protocol == "https") { + this->port = 443; + } + + // cut the host part + String _host; + + index = url.indexOf('/'); + if (index >= 0) { + _host = url.substring(0, index); + } else { + _host = url; + } + + // store the remaining part as path + if (index >= 0) { + url.remove(0, index); + this->path = url; + } else { + this->path = "/"; + } + + // separate host from port, when present + index = _host.indexOf(':'); + if (index >= 0) { + this->port = _host.substring(index + 1).toInt(); + this->host = _host.substring(0, index); + } else { + this->host = _host; + } + +} diff --git a/code/espurna/light.h b/code/espurna/light.h new file mode 100644 index 00000000..464a791d --- /dev/null +++ b/code/espurna/light.h @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------------- +// Light +// ----------------------------------------------------------------------------- + +#pragma once + +namespace Light { + constexpr const unsigned char VALUE_MIN = LIGHT_MIN_VALUE; + constexpr const unsigned char VALUE_MAX = LIGHT_MAX_VALUE; + + constexpr const unsigned int BRIGHTNESS_MIN = LIGHT_MIN_BRIGHTNESS; + constexpr const unsigned int BRIGHTNESS_MAX = LIGHT_MAX_BRIGHTNESS; + + // Default to the Philips Hue value that HA also use. + // https://developers.meethue.com/documentation/core-concepts + constexpr const unsigned int MIREDS_COLDWHITE = LIGHT_COLDWHITE_MIRED; + constexpr const unsigned int MIREDS_WARMWHITE = LIGHT_WARMWHITE_MIRED; + + constexpr const unsigned int KELVIN_WARMWHITE = LIGHT_WARMWHITE_KELVIN; + constexpr const unsigned int KELVIN_COLDWHITE = LIGHT_COLDWHITE_KELVIN; + + constexpr const unsigned int PWM_MIN = LIGHT_MIN_PWM; + constexpr const unsigned int PWM_MAX = LIGHT_MAX_PWM; + constexpr const unsigned int PWM_LIMIT = LIGHT_LIMIT_PWM; +} + diff --git a/code/espurna/light.ino b/code/espurna/light.ino index fadef91c..76593998 100644 --- a/code/espurna/light.ino +++ b/code/espurna/light.ino @@ -8,7 +8,10 @@ Copyright (C) 2016-2019 by Xose Pérez #if LIGHT_PROVIDER != LIGHT_PROVIDER_NONE +#include "light.h" + #include +#include #include #include @@ -29,15 +32,15 @@ Ticker _light_comms_ticker; Ticker _light_save_ticker; Ticker _light_transition_ticker; -typedef struct { - unsigned char pin; - bool reverse; - bool state; - unsigned char inputValue; // value that has been inputted - unsigned char value; // normalized value including brightness +struct channel_t { + unsigned char pin; // real GPIO pin + bool reverse; // wether we should invert the value before using it + bool state; // is the channel ON + unsigned char inputValue; // raw value, without the brightness + unsigned char value; // normalized value, including brightness unsigned char target; // target value double current; // transition value -} channel_t; +}; std::vector _light_channel; bool _light_dirty = false; @@ -48,9 +51,20 @@ bool _light_has_color = false; bool _light_use_white = false; bool _light_use_cct = false; bool _light_use_gamma = false; + +bool _light_provider_update = false; + +bool _light_use_transitions = false; +unsigned int _light_transition_time = LIGHT_TRANSITION_TIME; unsigned long _light_steps_left = 1; -unsigned char _light_brightness = LIGHT_MAX_BRIGHTNESS; -unsigned int _light_mireds = round((LIGHT_COLDWHITE_MIRED+LIGHT_WARMWHITE_MIRED)/2); + +bool _light_dirty = false; +bool _light_state = false; +unsigned char _light_brightness = Light::BRIGHTNESS_MAX; +unsigned int _light_mireds = lround((Light::MIREDS_COLDWHITE + Light::MIREDS_WARMWHITE) / 2); + +using light_brightness_func_t = void(); +light_brightness_func_t* _light_brightness_func = nullptr; #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX #include @@ -58,9 +72,18 @@ my92xx * _my92xx; ARRAYINIT(unsigned char, _light_channel_map, MY92XX_MAPPING); #endif +// UI hint about channel distribution +const char _light_channel_desc[5][5] PROGMEM = { + {'W', 0, 0, 0, 0}, + {'W', 'C', 0, 0, 0}, + {'R', 'G', 'B', 0, 0}, + {'R', 'G', 'B', 'W', 0}, + {'R', 'G', 'B', 'W', 'C'} +}; +static_assert((LIGHT_CHANNELS * LIGHT_CHANNELS) <= (sizeof(_light_channel_desc)), "Out-of-bounds array access"); + // Gamma Correction lookup table (8 bit) -// TODO: move to PROGMEM -const unsigned char _light_gamma_table[] = { +const unsigned char _light_gamma_table[] PROGMEM = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, @@ -78,19 +101,20 @@ const unsigned char _light_gamma_table[] = { 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 238, 240, 242, 244, 246, 248, 251, 253, 255 }; +static_assert(Light::VALUE_MAX <= sizeof(_light_gamma_table), "Out-of-bounds array access"); // ----------------------------------------------------------------------------- // UTILS // ----------------------------------------------------------------------------- -void _setValue(unsigned char id, unsigned int value) { +void _setValue(const unsigned char id, const unsigned int value) { if (_light_channel[id].value != value) { _light_channel[id].value = value; _light_dirty = true; } } -void _setInputValue(unsigned char id, unsigned int value) { +void _setInputValue(const unsigned char id, const unsigned int value) { if (_light_channel[id].inputValue != value) { _light_channel[id].inputValue = value; //_light_dirty = true; @@ -98,76 +122,98 @@ void _setInputValue(unsigned char id, unsigned int value) { } void _setRGBInputValue(unsigned char red, unsigned char green, unsigned char blue) { - _setInputValue(0, constrain(red, 0, LIGHT_MAX_VALUE)); - _setInputValue(1, constrain(green, 0, LIGHT_MAX_VALUE)); - _setInputValue(2, constrain(blue, 0, LIGHT_MAX_VALUE)); + _setInputValue(0, constrain(red, Light::VALUE_MIN, Light::VALUE_MAX)); + _setInputValue(1, constrain(green, Light::VALUE_MIN, Light::VALUE_MAX)); + _setInputValue(2, constrain(blue, Light::VALUE_MIN, Light::VALUE_MAX)); } -void _generateBrightness() { - - double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS; +void _setCCTInputValue(unsigned char warm, unsigned char cold) { + _setInputValue(0, constrain(warm, Light::VALUE_MIN, Light::VALUE_MAX)); + _setInputValue(1, constrain(cold, Light::VALUE_MIN, Light::VALUE_MAX)); +} - // Convert RGB to RGBW(W) - if (_light_has_color && _light_use_white) { +void _lightApplyBrightness(unsigned char channels = lightChannels()) { - // Substract the common part from RGB channels and add it to white channel. So [250,150,50] -> [200,100,0,50] - unsigned char white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue)); - for (unsigned int i=0; i < 3; i++) { - _setValue(i, _light_channel[i].inputValue - white); - } + double brightness = static_cast(_light_brightness) / static_cast(Light::BRIGHTNESS_MAX); - // Split the White Value across 2 White LED Strips. - if (_light_use_cct) { + channels = std::min(channels, lightChannels()); - // This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end. - double miredFactor = ((double) _light_mireds - (double) LIGHT_COLDWHITE_MIRED)/((double) LIGHT_WARMWHITE_MIRED - (double) LIGHT_COLDWHITE_MIRED); + for (unsigned char i=0; i < lightChannels(); i++) { + if (i >= channels) brightness = 1; + _setValue(i, _light_channel[i].inputValue * brightness); + } - // set cold white - _light_channel[3].inputValue = 0; - _setValue(3, round(((double) 1.0 - miredFactor) * white)); +} - // set warm white - _light_channel[4].inputValue = 0; - _setValue(4, round(miredFactor * white)); - } else { - _light_channel[3].inputValue = 0; - _setValue(3, white); - } +void _lightApplyBrightnessColor() { - // Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63] - unsigned char max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue)); - unsigned char max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value)); - unsigned char channelSize = _light_use_cct ? 5 : 4; + double brightness = static_cast(_light_brightness) / static_cast(Light::BRIGHTNESS_MAX); - if (_light_use_cct) { - max_out = std::max(max_out, _light_channel[4].value); - } + // Substract the common part from RGB channels and add it to white channel. So [250,150,50] -> [200,100,0,50] + unsigned char white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue)); + for (unsigned int i=0; i < 3; i++) { + _setValue(i, _light_channel[i].inputValue - white); + } - double factor = (max_out > 0) ? (double) (max_in / max_out) : 0; - for (unsigned char i=0; i < channelSize; i++) { - _setValue(i, round((double) _light_channel[i].value * factor * brightness)); - } + // Split the White Value across 2 White LED Strips. + if (_light_use_cct) { - // Scale white channel to match brightness - for (unsigned char i=3; i < channelSize; i++) { - _setValue(i, constrain(_light_channel[i].value * LIGHT_WHITE_FACTOR, 0, LIGHT_MAX_BRIGHTNESS)); - } + // This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end. + double miredFactor = ((double) _light_mireds - (double) Light::MIREDS_COLDWHITE)/((double) Light::MIREDS_WARMWHITE - (double) Light::MIREDS_COLDWHITE); - // For the rest of channels, don't apply brightness, it is already in the inputValue - // i should be 4 when RGBW and 5 when RGBWW - for (unsigned char i=channelSize; i < _light_channel.size(); i++) { - _setValue(i, _light_channel[i].inputValue); - } + // set cold white + _light_channel[3].inputValue = 0; + _setValue(3, lround(((double) 1.0 - miredFactor) * white)); + // set warm white + _light_channel[4].inputValue = 0; + _setValue(4, lround(miredFactor * white)); } else { + _light_channel[3].inputValue = 0; + _setValue(3, white); + } - // Apply brightness equally to all channels - for (unsigned char i=0; i < _light_channel.size(); i++) { - _setValue(i, _light_channel[i].inputValue * brightness); - } + // Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63] + unsigned char max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue)); + unsigned char max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value)); + unsigned char channelSize = _light_use_cct ? 5 : 4; + + if (_light_use_cct) { + max_out = std::max(max_out, _light_channel[4].value); + } + double factor = (max_out > 0) ? (double) (max_in / max_out) : 0; + for (unsigned char i=0; i < channelSize; i++) { + _setValue(i, lround((double) _light_channel[i].value * factor * brightness)); } + // Scale white channel to match brightness + for (unsigned char i=3; i < channelSize; i++) { + _setValue(i, constrain(static_cast(_light_channel[i].value * LIGHT_WHITE_FACTOR), Light::BRIGHTNESS_MIN, Light::BRIGHTNESS_MAX)); + } + + // For the rest of channels, don't apply brightness, it is already in the inputValue + // i should be 4 when RGBW and 5 when RGBWW + for (unsigned char i=channelSize; i < _light_channel.size(); i++) { + _setValue(i, _light_channel[i].inputValue); + } + +} + +String lightDesc(unsigned char id) { + if (id >= _light_channel.size()) return F("UNKNOWN"); + + const char tag = pgm_read_byte(&_light_channel_desc[_light_channel.size() - 1][id]); + switch (tag) { + case 'W': return F("WARM WHITE"); + case 'C': return F("COLD WHITE"); + case 'R': return F("RED"); + case 'G': return F("GREEN"); + case 'B': return F("BLUE"); + default: break; + } + + return F("UNKNOWN"); } // ----------------------------------------------------------------------------- @@ -177,7 +223,7 @@ void _generateBrightness() { void _fromLong(unsigned long value, bool brightness) { if (brightness) { _setRGBInputValue((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF); - _light_brightness = (value & 0xFF) * LIGHT_MAX_BRIGHTNESS / 255; + _light_brightness = (value & 0xFF) * Light::BRIGHTNESS_MAX / 255; } else { _setRGBInputValue((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value) & 0xFF); } @@ -216,12 +262,12 @@ void _fromRGB(const char * rgb) { // RGB but less than 3 values received, assume it is 0 if (_light_has_color && (count < 3)) { - // check channel 1 and 2: - for (int i = 1; i <= 2; i++) { - if (count < (i+1)) { - _setInputValue(i, 0); + // check channel 1 and 2: + for (int i = 1; i <= 2; i++) { + if (count < (i+1)) { + _setInputValue(i, 0); + } } - } } break; } @@ -259,32 +305,32 @@ void _fromHSV(const char * hsv) { double f = (h - floor(h)); double s = (double) value[1] / 100.0; - _light_brightness = round((double) value[2] * 2.55); // (255/100) - unsigned char p = round(255 * (1.0 - s)); - unsigned char q = round(255 * (1.0 - s * f)); - unsigned char t = round(255 * (1.0 - s * (1.0 - f))); + _light_brightness = lround((double) value[2] * (static_cast(Light::BRIGHTNESS_MAX) / 100.0)); // (default 255/100) + unsigned char p = lround(Light::VALUE_MAX * (1.0 - s)); + unsigned char q = lround(Light::VALUE_MAX * (1.0 - s * f)); + unsigned char t = lround(Light::VALUE_MAX * (1.0 - s * (1.0 - f))); switch (int(h)) { case 0: - _setRGBInputValue(255, t, p); + _setRGBInputValue(Light::VALUE_MAX, t, p); break; case 1: - _setRGBInputValue(q, 255, p); + _setRGBInputValue(q, Light::VALUE_MAX, p); break; case 2: - _setRGBInputValue(p, 255, t); + _setRGBInputValue(p, Light::VALUE_MAX, t); break; case 3: - _setRGBInputValue(p, q, 255); + _setRGBInputValue(p, q, Light::VALUE_MAX); break; case 4: - _setRGBInputValue(t, p, 255); + _setRGBInputValue(t, p, Light::VALUE_MAX); break; case 5: - _setRGBInputValue(255, p, q); + _setRGBInputValue(Light::VALUE_MAX, p, q); break; default: - _setRGBInputValue(0, 0, 0); + _setRGBInputValue(Light::VALUE_MIN, Light::VALUE_MIN, Light::VALUE_MIN); break; } } @@ -293,25 +339,39 @@ void _fromHSV(const char * hsv) { // https://github.com/stelgenhof/AiLight void _fromKelvin(unsigned long kelvin) { - if (!_light_has_color) return; + if (!_light_has_color) { + + if(!_light_use_cct) return; + + _light_mireds = constrain(static_cast(lround(1000000UL / kelvin)), Light::MIREDS_COLDWHITE, Light::MIREDS_WARMWHITE); + + // This change the range from 153-500 to 0-347 so we get a value between 0 and 1 in the end. + double factor = ((double) _light_mireds - (double) Light::MIREDS_COLDWHITE)/((double) Light::MIREDS_WARMWHITE - (double) Light::MIREDS_COLDWHITE); + unsigned char warm = lround(factor * Light::VALUE_MAX); + unsigned char cold = lround(((double) 1.0 - factor) * Light::VALUE_MAX); + + _setCCTInputValue(warm, cold); + + return; + } - _light_mireds = constrain(round(1000000UL / kelvin), LIGHT_MIN_MIREDS, LIGHT_MAX_MIREDS); + _light_mireds = constrain(static_cast(lround(1000000UL / kelvin)), Light::MIREDS_COLDWHITE, Light::MIREDS_WARMWHITE); if (_light_use_cct) { - _setRGBInputValue(LIGHT_MAX_VALUE, LIGHT_MAX_VALUE, LIGHT_MAX_VALUE); + _setRGBInputValue(Light::VALUE_MAX, Light::VALUE_MAX, Light::VALUE_MAX); return; } // Calculate colors kelvin /= 100; unsigned int red = (kelvin <= 66) - ? LIGHT_MAX_VALUE + ? Light::VALUE_MAX : 329.698727446 * fs_pow((double) (kelvin - 60), -0.1332047592); unsigned int green = (kelvin <= 66) ? 99.4708025861 * fs_log(kelvin) - 161.1195681661 : 288.1221695283 * fs_pow((double) kelvin, -0.0755148492); unsigned int blue = (kelvin >= 66) - ? LIGHT_MAX_VALUE + ? Light::VALUE_MAX : ((kelvin <= 19) ? 0 : 138.5177312231 * fs_log(kelvin - 10) - 305.0447927307); @@ -322,7 +382,7 @@ void _fromKelvin(unsigned long kelvin) { // Color temperature is measured in mireds (kelvin = 1e6/mired) void _fromMireds(unsigned long mireds) { - unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000); + unsigned long kelvin = constrain(static_cast(1000000UL / mireds), Light::KELVIN_WARMWHITE, Light::KELVIN_COLDWHITE); _fromKelvin(kelvin); } @@ -330,7 +390,7 @@ void _fromMireds(unsigned long mireds) { // Output Values // ----------------------------------------------------------------------------- -void _toRGB(char * rgb, size_t len, bool target) { +void _toRGB(char * rgb, size_t len, bool target = false) { unsigned long value = 0; value += target ? _light_channel[0].target : _light_channel[0].inputValue; @@ -342,20 +402,17 @@ void _toRGB(char * rgb, size_t len, bool target) { snprintf_P(rgb, len, PSTR("#%06X"), value); } -void _toRGB(char * rgb, size_t len) { - _toRGB(rgb, len, false); -} - -void _toHSV(char * hsv, size_t len, bool target) { - double h, s, v; - double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS; +void _toHSV(char * hsv, size_t len) { + double h {0.}, s {0.}, v {0.}; + double r {0.}, g {0.}, b {0.}; + double min {0.}, max {0.}; - double r = (double) ((target ? _light_channel[0].target : _light_channel[0].inputValue) * brightness) / 255.0; - double g = (double) ((target ? _light_channel[1].target : _light_channel[1].inputValue) * brightness) / 255.0; - double b = (double) ((target ? _light_channel[2].target : _light_channel[2].inputValue) * brightness) / 255.0; + r = static_cast(_light_channel[0].target) / Light::VALUE_MAX; + g = static_cast(_light_channel[1].target) / Light::VALUE_MAX; + b = static_cast(_light_channel[2].target) / Light::VALUE_MAX; - double min = std::min(r, std::min(g, b)); - double max = std::max(r, std::max(g, b)); + min = std::min(r, std::min(g, b)); + max = std::max(r, std::max(g, b)); v = 100.0 * max; if (v == 0) { @@ -379,22 +436,22 @@ void _toHSV(char * hsv, size_t len, bool target) { } } - // String - snprintf_P(hsv, len, PSTR("%d,%d,%d"), round(h), round(s), round(v)); -} - -void _toHSV(char * hsv, size_t len) { - _toHSV(hsv, len, false); + // Convert to string. Using lround, since we can't (yet) printf floats + snprintf(hsv, len, "%d,%d,%d", + static_cast(lround(h)), + static_cast(lround(s)), + static_cast(lround(v)) + ); } void _toLong(char * color, size_t len, bool target) { - + if (!_light_has_color) return; - snprintf_P(color, len, PSTR("%d,%d,%d"), - (int) (target ? _light_channel[0].target : _light_channel[0].inputValue), - (int) (target ? _light_channel[1].target : _light_channel[1].inputValue), - (int) (target ? _light_channel[2].target : _light_channel[2].inputValue) + snprintf_P(color, len, PSTR("%u,%u,%u"), + (target ? _light_channel[0].target : _light_channel[0].inputValue), + (target ? _light_channel[1].target : _light_channel[1].inputValue), + (target ? _light_channel[2].target : _light_channel[2].inputValue) ); } @@ -405,7 +462,7 @@ void _toLong(char * color, size_t len) { void _toCSV(char * buffer, size_t len, bool applyBrightness, bool target) { char num[10]; - float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1; + float b = applyBrightness ? (float) _light_brightness / Light::BRIGHTNESS_MAX : 1; for (unsigned char i=0; i<_light_channel.size(); i++) { itoa((target ? _light_channel[i].target : _light_channel[i].inputValue) * b, num, 10); if (i>0) strncat(buffer, ",", len--); @@ -418,14 +475,24 @@ void _toCSV(char * buffer, size_t len, bool applyBrightness) { _toCSV(buffer, len, applyBrightness, false); } +// See cores/esp8266/WMath.cpp::map +// Redefining as local method here to avoid breaking in unexpected ways in inputs like (0, 0, 0, 0, 1) +template T _lightMap(T x, Tin in_min, Tin in_max, Tout out_min, Tout out_max) { + auto divisor = (in_max - in_min); + if (divisor == 0){ + return -1; //AVR returns -1, SAM returns 0 + } + return (x - in_min) * (out_max - out_min) / divisor + out_min; +} + // ----------------------------------------------------------------------------- // PROVIDER // ----------------------------------------------------------------------------- -unsigned int _toPWM(unsigned long value, bool gamma, bool reverse) { - value = constrain(value, 0, LIGHT_MAX_VALUE); - if (gamma) value = _light_gamma_table[value]; - if (LIGHT_MAX_VALUE != LIGHT_LIMIT_PWM) value = map(value, 0, LIGHT_MAX_VALUE, 0, LIGHT_LIMIT_PWM); +unsigned int _toPWM(unsigned int value, bool gamma, bool reverse) { + value = constrain(value, Light::VALUE_MIN, Light::VALUE_MAX); + if (gamma) value = pgm_read_byte(_light_gamma_table + value); + if (Light::VALUE_MAX != Light::PWM_LIMIT) value = _lightMap(value, Light::VALUE_MIN, Light::VALUE_MAX, Light::PWM_MIN, Light::PWM_LIMIT); if (reverse) value = LIGHT_LIMIT_PWM - value; return value; } @@ -458,6 +525,10 @@ void _transition() { void _lightProviderUpdate() { + if (_light_provider_update) return; + + _light_provider_update = true; + _transition(); #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX @@ -479,6 +550,12 @@ void _lightProviderUpdate() { #endif + _light_provider_update = false; + +} + +void _lightProviderDoUpdate() { + schedule_function(_lightProviderUpdate); } // ----------------------------------------------------------------------------- @@ -536,11 +613,10 @@ void _lightSaveSettings() { void _lightRestoreSettings() { for (unsigned int i=0; i < _light_channel.size(); i++) { - _light_channel[i].inputValue = getSetting("ch", i, i==0 ? 255 : 0).toInt(); + _light_channel[i].inputValue = getSetting("ch", i, (i == 0) ? Light::VALUE_MAX : 0).toInt(); } - _light_brightness = getSetting("brightness", LIGHT_MAX_BRIGHTNESS).toInt(); + _light_brightness = getSetting("brightness", Light::BRIGHTNESS_MAX).toInt(); _light_mireds = getSetting("mireds", _light_mireds).toInt(); - lightUpdate(false, false); } // ----------------------------------------------------------------------------- @@ -557,13 +633,16 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl mqttSubscribe(MQTT_TOPIC_BRIGHTNESS); if (_light_has_color) { - mqttSubscribe(MQTT_TOPIC_MIRED); - mqttSubscribe(MQTT_TOPIC_KELVIN); mqttSubscribe(MQTT_TOPIC_COLOR_RGB); mqttSubscribe(MQTT_TOPIC_COLOR_HSV); mqttSubscribe(MQTT_TOPIC_TRANSITION); } + if (_light_has_color || _light_use_cct) { + mqttSubscribe(MQTT_TOPIC_MIRED); + mqttSubscribe(MQTT_TOPIC_KELVIN); + } + // Group color if (mqtt_group_color.length() > 0) mqttSubscribeRaw(mqtt_group_color.c_str()); @@ -614,7 +693,7 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl // Brightness if (t.equals(MQTT_TOPIC_BRIGHTNESS)) { - _light_brightness = constrain(atoi(payload), 0, LIGHT_MAX_BRIGHTNESS); + lightBrightness(atoi(payload)); lightUpdate(true, mqttForward()); return; } @@ -655,13 +734,17 @@ void lightMQTT() { } mqttSend(MQTT_TOPIC_COLOR_RGB, buffer); - _toHSV(buffer, sizeof(buffer), true); + _toHSV(buffer, sizeof(buffer)); mqttSend(MQTT_TOPIC_COLOR_HSV, buffer); - // Mireds - snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds); - mqttSend(MQTT_TOPIC_MIRED, buffer); - + } + + if (_light_has_color || _light_use_cct) { + + // Mireds + snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds); + mqttSend(MQTT_TOPIC_MIRED, buffer); + } // Channels @@ -715,6 +798,10 @@ bool lightHasColor() { return _light_has_color; } +bool lightUseCCT() { + return _light_use_cct; +} + void _lightComms(unsigned char mask) { // Report color & brightness to MQTT broker @@ -725,7 +812,7 @@ void _lightComms(unsigned char mask) { // Report color to WS clients (using current brightness setting) #if WEB_SUPPORT - wsSend(_lightWebSocketStatus); + wsPost(_lightWebSocketStatus); #endif // Report channels to local broker @@ -738,7 +825,7 @@ void _lightComms(unsigned char mask) { void lightUpdate(bool save, bool forward, bool group_forward) { // Calculate values based on inputs and brightness - _generateBrightness(); + _light_brightness_func(); // Only update if a channel has changed if (!_light_dirty) return; @@ -752,7 +839,7 @@ void lightUpdate(bool save, bool forward, bool group_forward) { // Configure color transition _light_steps_left = _light_use_transitions ? _light_transition_time / LIGHT_TRANSITION_STEP : 1; - _light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderUpdate); + _light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderDoUpdate); // Delay every communication 100ms to avoid jamming unsigned char mask = 0; @@ -839,9 +926,9 @@ unsigned int lightChannel(unsigned char id) { return 0; } -void lightChannel(unsigned char id, int value) { +void lightChannel(unsigned char id, unsigned char value) { if (id <= _light_channel.size()) { - _light_channel[id].inputValue = constrain(value, 0, LIGHT_MAX_VALUE); + _setInputValue(id, constrain(value, Light::VALUE_MIN, Light::VALUE_MAX)); } } @@ -853,15 +940,15 @@ unsigned int lightBrightness() { return _light_brightness; } -void lightBrightness(int b) { - _light_brightness = constrain(b, 0, LIGHT_MAX_BRIGHTNESS); +void lightBrightness(unsigned int brightness) { + _light_brightness = constrain(brightness, Light::BRIGHTNESS_MIN, Light::BRIGHTNESS_MAX); } void lightBrightnessStep(int steps) { lightBrightness(_light_brightness + steps * LIGHT_STEP); } -unsigned long lightTransitionTime() { +unsigned int lightTransitionTime() { if (_light_use_transitions) { return _light_transition_time; } else { @@ -887,7 +974,7 @@ void lightTransitionTime(unsigned long m) { #if WEB_SUPPORT -bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _lightWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "light", 5) == 0) return true; if (strncmp(key, "use", 3) == 0) return true; return false; @@ -895,16 +982,16 @@ bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) { void _lightWebSocketStatus(JsonObject& root) { if (_light_has_color) { - if (_light_use_cct) { - root["useCCT"] = _light_use_cct; - root["mireds"] = _light_mireds; - } if (getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1) { root["rgb"] = lightColor(true); } else { root["hsv"] = lightColor(false); } } + if (_light_use_cct) { + root["useCCT"] = _light_use_cct; + root["mireds"] = _light_mireds; + } JsonArray& channels = root.createNestedArray("channels"); for (unsigned char id=0; id < _light_channel.size(); id++) { channels.add(lightChannel(id)); @@ -912,8 +999,11 @@ void _lightWebSocketStatus(JsonObject& root) { root["brightness"] = lightBrightness(); } -void _lightWebSocketOnSend(JsonObject& root) { +void _lightWebSocketOnVisible(JsonObject& root) { root["colorVisible"] = 1; +} + +void _lightWebSocketOnConnected(JsonObject& root) { root["mqttGroupColor"] = getSetting("mqttGroupColor"); root["useColor"] = _light_has_color; root["useWhite"] = _light_use_white; @@ -939,14 +1029,16 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject lightUpdate(true, true); } } - if (_light_use_cct) { - if (strcmp(action, "mireds") == 0) { - _fromMireds(data["mireds"]); - lightUpdate(true, true); - } - } } + if (_light_use_cct) { + if (strcmp(action, "mireds") == 0) { + _fromMireds(data["mireds"]); + lightUpdate(true, true); + } + } + + if (strcmp(action, "channel") == 0) { if (data.containsKey("id") && data.containsKey("value")) { lightChannel(data["id"], data["value"]); @@ -987,7 +1079,7 @@ void _lightAPISetup() { apiRegister(MQTT_TOPIC_COLOR_HSV, [](char * buffer, size_t len) { - _toHSV(buffer, len, true); + _toHSV(buffer, len); }, [](const char * payload) { lightColor(payload, false); @@ -1082,7 +1174,7 @@ void _lightInitCommands() { lightChannel(id, value); lightUpdate(true, true); } - DEBUG_MSG_P(PSTR("Channel #%d: %d\n"), id, lightChannel(id)); + DEBUG_MSG_P(PSTR("Channel #%d (%s): %d\n"), id, lightDesc(id).c_str(), lightChannel(id)); terminalOK(); }); @@ -1129,26 +1221,19 @@ void _lightInitCommands() { #endif // TERMINAL_SUPPORT #if LIGHT_PROVIDER == LIGHT_PROVIDER_DIMMER +const unsigned long _light_iomux[16] PROGMEM = { + PERIPHS_IO_MUX_GPIO0_U, PERIPHS_IO_MUX_U0TXD_U, PERIPHS_IO_MUX_GPIO2_U, PERIPHS_IO_MUX_U0RXD_U, + PERIPHS_IO_MUX_GPIO4_U, PERIPHS_IO_MUX_GPIO5_U, PERIPHS_IO_MUX_SD_CLK_U, PERIPHS_IO_MUX_SD_DATA0_U, + PERIPHS_IO_MUX_SD_DATA1_U, PERIPHS_IO_MUX_SD_DATA2_U, PERIPHS_IO_MUX_SD_DATA3_U, PERIPHS_IO_MUX_SD_CMD_U, + PERIPHS_IO_MUX_MTDI_U, PERIPHS_IO_MUX_MTCK_U, PERIPHS_IO_MUX_MTMS_U, PERIPHS_IO_MUX_MTDO_U +}; -unsigned long getIOMux(unsigned long gpio) { - unsigned long muxes[16] = { - PERIPHS_IO_MUX_GPIO0_U, PERIPHS_IO_MUX_U0TXD_U, PERIPHS_IO_MUX_GPIO2_U, PERIPHS_IO_MUX_U0RXD_U, - PERIPHS_IO_MUX_GPIO4_U, PERIPHS_IO_MUX_GPIO5_U, PERIPHS_IO_MUX_SD_CLK_U, PERIPHS_IO_MUX_SD_DATA0_U, - PERIPHS_IO_MUX_SD_DATA1_U, PERIPHS_IO_MUX_SD_DATA2_U, PERIPHS_IO_MUX_SD_DATA3_U, PERIPHS_IO_MUX_SD_CMD_U, - PERIPHS_IO_MUX_MTDI_U, PERIPHS_IO_MUX_MTCK_U, PERIPHS_IO_MUX_MTMS_U, PERIPHS_IO_MUX_MTDO_U - }; - return muxes[gpio]; -} - -unsigned long getIOFunc(unsigned long gpio) { - unsigned long funcs[16] = { - FUNC_GPIO0, FUNC_GPIO1, FUNC_GPIO2, FUNC_GPIO3, - FUNC_GPIO4, FUNC_GPIO5, FUNC_GPIO6, FUNC_GPIO7, - FUNC_GPIO8, FUNC_GPIO9, FUNC_GPIO10, FUNC_GPIO11, - FUNC_GPIO12, FUNC_GPIO13, FUNC_GPIO14, FUNC_GPIO15 - }; - return funcs[gpio]; -} +const unsigned long _light_iofunc[16] PROGMEM = { + FUNC_GPIO0, FUNC_GPIO1, FUNC_GPIO2, FUNC_GPIO3, + FUNC_GPIO4, FUNC_GPIO5, FUNC_GPIO6, FUNC_GPIO7, + FUNC_GPIO8, FUNC_GPIO9, FUNC_GPIO10, FUNC_GPIO11, + FUNC_GPIO12, FUNC_GPIO13, FUNC_GPIO14, FUNC_GPIO15 +}; #endif @@ -1161,13 +1246,23 @@ void _lightConfigure() { } _light_use_white = getSetting("useWhite", LIGHT_USE_WHITE).toInt() == 1; - if (_light_use_white && (_light_channel.size() < 4)) { + if (_light_use_white && (_light_channel.size() < 4) && (_light_channel.size() != 2)) { _light_use_white = false; setSetting("useWhite", _light_use_white); } + if (_light_has_color) { + if (_light_use_white) { + _light_brightness_func = _lightApplyBrightnessColor; + } else { + _light_brightness_func = []() { _lightApplyBrightness(3); }; + } + } else { + _light_brightness_func = []() { _lightApplyBrightness(); }; + } + _light_use_cct = getSetting("useCCT", LIGHT_USE_CCT).toInt() == 1; - if (_light_use_cct && ((_light_channel.size() < 5) || !_light_use_white)) { + if (_light_use_cct && (((_light_channel.size() < 5) && (_light_channel.size() != 2)) || !_light_use_white)) { _light_use_cct = false; setSetting("useCCT", _light_use_cct); } @@ -1185,6 +1280,8 @@ void lightSetup() { digitalWrite(LIGHT_ENABLE_PIN, HIGH); #endif + _light_channel.reserve(LIGHT_CHANNELS); + #if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX _my92xx = new my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND); @@ -1219,11 +1316,12 @@ void lightSetup() { uint32 pwm_duty_init[PWM_CHANNEL_NUM_MAX]; uint32 io_info[PWM_CHANNEL_NUM_MAX][3]; for (unsigned int i=0; i < _light_channel.size(); i++) { + const auto pin = _light_channel.at(i).pin; pwm_duty_init[i] = 0; - io_info[i][0] = getIOMux(_light_channel[i].pin); - io_info[i][1] = getIOFunc(_light_channel[i].pin); - io_info[i][2] = _light_channel[i].pin; - pinMode(_light_channel[i].pin, OUTPUT); + io_info[i][0] = pgm_read_dword(&_light_iomux[pin]); + io_info[i][1] = pgm_read_dword(&_light_iofunc[pin]); + io_info[i][2] = pin; + pinMode(pin, OUTPUT); } pwm_init(LIGHT_MAX_PWM, pwm_duty_init, PWM_CHANNEL_NUM_MAX, io_info); pwm_start(); @@ -1240,11 +1338,14 @@ void lightSetup() { } else { _lightRestoreSettings(); } + lightUpdate(false, false); #if WEB_SUPPORT - wsOnSendRegister(_lightWebSocketOnSend); - wsOnActionRegister(_lightWebSocketOnAction); - wsOnReceiveRegister(_lightWebSocketOnReceive); + wsRegister() + .onVisible(_lightWebSocketOnVisible) + .onConnected(_lightWebSocketOnConnected) + .onAction(_lightWebSocketOnAction) + .onKeyCheck(_lightWebSocketOnKeyCheck); #endif #if API_SUPPORT diff --git a/code/espurna/lightfox.ino b/code/espurna/lightfox.ino index 7814f1e8..5a6dd1c6 100644 --- a/code/espurna/lightfox.ino +++ b/code/espurna/lightfox.ino @@ -47,7 +47,7 @@ void lightfoxClear() { #if WEB_SUPPORT -void _lightfoxWebSocketOnSend(JsonObject& root) { +void _lightfoxWebSocketOnConnected(JsonObject& root) { root["lightfoxVisible"] = 1; uint8_t buttonsCount = _buttons.size(); root["lightfoxRelayCount"] = relayCount(); @@ -94,8 +94,9 @@ void _lightfoxInitCommands() { void lightfoxSetup() { #if WEB_SUPPORT - wsOnSendRegister(_lightfoxWebSocketOnSend); - wsOnActionRegister(_lightfoxWebSocketOnAction); + wsRegister() + .onConnected(_lightfoxWebSocketOnConnected) + .onAction(_lightfoxWebSocketOnAction); #endif #if TERMINAL_SUPPORT diff --git a/code/espurna/mdns.ino b/code/espurna/mdns.ino index bc174ee6..ff8cb175 100644 --- a/code/espurna/mdns.ino +++ b/code/espurna/mdns.ino @@ -50,6 +50,7 @@ void mdnsServerSetup() { // Public ESPurna related txt for OTA discovery MDNS.addServiceTxt("arduino", "tcp", "app_name", APP_NAME); MDNS.addServiceTxt("arduino", "tcp", "app_version", APP_VERSION); + MDNS.addServiceTxt("arduino", "tcp", "build_date", buildTime()); MDNS.addServiceTxt("arduino", "tcp", "mac", WiFi.macAddress()); MDNS.addServiceTxt("arduino", "tcp", "target_board", getBoardName()); { diff --git a/code/espurna/migrate.ino b/code/espurna/migrate.ino index 0f084b55..68b49675 100644 --- a/code/espurna/migrate.ino +++ b/code/espurna/migrate.ino @@ -1315,6 +1315,18 @@ void migrate() { setSetting("chLogic", 3, 0); setSetting("relays", 1); + #elif defined(ISELECTOR_SM_PW702) + + setSetting("board", 98); + setSetting("ledGPIO", 0, 4); + setSetting("ledLogic", 0, 0); + setSetting("ledGPIO", 1, 5); + setSetting("ledLogic", 0, 0); + setSetting("btnGPIO", 0, 13); + setSetting("btnRelay", 0, 0); + setSetting("relayGPIO", 0, 12); + setSetting("relayType", 0, RELAY_TYPE_NORMAL); + #else // Allow users to define new settings without migration config diff --git a/code/espurna/mqtt.ino b/code/espurna/mqtt.ino index e4db050f..4f876581 100644 --- a/code/espurna/mqtt.ino +++ b/code/espurna/mqtt.ino @@ -3,6 +3,7 @@ MQTT MODULE Copyright (C) 2016-2019 by Xose Pérez +Updated secure client support by Niek van der Maas < mail at niekvandermaas dot nl> */ @@ -13,30 +14,50 @@ Copyright (C) 2016-2019 by Xose Pérez #include #include #include +#include #include +#include -#if MQTT_USE_ASYNC // Using AsyncMqttClient +#include "libs/SecureClientHelpers.h" -#include -AsyncMqttClient _mqtt; +#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT -#else // Using PubSubClient + AsyncMqttClient _mqtt; -#include -PubSubClient _mqtt; -bool _mqtt_connected = false; +#else // MQTT_LIBRARY_ARDUINOMQTT / MQTT_LIBRARY_PUBSUBCLIENT + + WiFiClient _mqtt_client; + +#if SECURE_CLIENT != SECURE_CLIENT_NONE + std::unique_ptr _mqtt_client_secure = nullptr; + + #if MQTT_SECURE_CLIENT_INCLUDE_CA + #include "static/mqtt_client_trusted_root_ca.h" // Assumes this header file defines a _mqtt_client_trusted_root_ca[] PROGMEM = "...PEM data..." + #else + #include "static/letsencrypt_isrgroot_pem.h" // Default to LetsEncrypt X3 certificate + #define _mqtt_client_trusted_root_ca _ssl_letsencrypt_isrg_x3_ca + #endif // MQTT_SECURE_CLIENT_INCLUDE_CA + +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE -WiFiClient _mqtt_client; -#if ASYNC_TCP_SSL_ENABLED -WiFiClientSecure _mqtt_client_secure; -#endif // ASYNC_TCP_SSL_ENABLED +#if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT +#ifdef MQTT_MAX_PACKET_SIZE + MQTTClient _mqtt(MQTT_MAX_PACKET_SIZE); +#else + MQTTClient _mqtt; +#endif +#elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT + PubSubClient _mqtt; +#endif + +#endif // MQTT_LIBRARY == MQTT_ASYNCMQTTCLIENT -#endif // MQTT_USE_ASYNC bool _mqtt_enabled = MQTT_ENABLED; bool _mqtt_use_json = false; unsigned long _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; unsigned long _mqtt_last_connection = 0; +bool _mqtt_connected = false; bool _mqtt_connecting = false; unsigned char _mqtt_qos = MQTT_QOS; bool _mqtt_retain = MQTT_RETAIN; @@ -46,18 +67,24 @@ String _mqtt_topic_json; String _mqtt_setter; String _mqtt_getter; bool _mqtt_forward; -char *_mqtt_user = 0; -char *_mqtt_pass = 0; -char *_mqtt_will; -char *_mqtt_clientid; +String _mqtt_user; +String _mqtt_pass; +String _mqtt_will; +String _mqtt_server; +uint16_t _mqtt_port; +String _mqtt_clientid; + +String _mqtt_payload_online; +String _mqtt_payload_offline; std::vector _mqtt_callbacks; -typedef struct { - unsigned char parent = 255; +struct mqtt_message_t { + static const unsigned char END = 255; + unsigned char parent = END; char * topic; char * message = NULL; -} mqtt_message_t; +}; std::vector _mqtt_queue; Ticker _mqtt_flush_ticker; @@ -65,6 +92,115 @@ Ticker _mqtt_flush_ticker; // Private // ----------------------------------------------------------------------------- +#if SECURE_CLIENT == SECURE_CLIENT_AXTLS +SecureClientConfig _mqtt_sc_config { + "MQTT", + []() -> String { + return _mqtt_server; + }, + []() -> int { + return getSetting("mqttScCheck", MQTT_SECURE_CLIENT_CHECK).toInt(); + }, + []() -> String { + return getSetting("mqttfp", MQTT_SSL_FINGERPRINT); + }, + true +}; +#endif + +#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL +SecureClientConfig _mqtt_sc_config { + "MQTT", + []() -> int { + return getSetting("mqttScCheck", MQTT_SECURE_CLIENT_CHECK).toInt(); + }, + []() -> PGM_P { + return _mqtt_client_trusted_root_ca; + }, + []() -> String { + return getSetting("mqttfp", MQTT_SSL_FINGERPRINT); + }, + []() -> uint16_t { + return getSetting("mqttScMFLN", MQTT_SECURE_CLIENT_MFLN).toInt(); + }, + true +}; +#endif + + +#if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + +void _mqttSetupAsyncClient(bool secure = false) { + + _mqtt.setServer(_mqtt_server.c_str(), _mqtt_port); + _mqtt.setClientId(_mqtt_clientid.c_str()); + _mqtt.setKeepAlive(_mqtt_keepalive); + _mqtt.setCleanSession(false); + _mqtt.setWill(_mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str()); + + if (_mqtt_user.length() && _mqtt_pass.length()) { + DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user.c_str()); + _mqtt.setCredentials(_mqtt_user.c_str(), _mqtt_pass.c_str()); + } + + #if SECURE_CLIENT != SECURE_CLIENT_NONE + if (secure) { + DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); + _mqtt.setSecure(secure); + } + #endif // SECURE_CLIENT != SECURE_CLIENT_NONE + + _mqtt.connect(); + +} + +#endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + +#if (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) +bool _mqttSetupSyncClient(bool secure = false) { + + #if SECURE_CLIENT != SECURE_CLIENT_NONE + if (secure) { + if (!_mqtt_client_secure) _mqtt_client_secure = std::make_unique(_mqtt_sc_config); + return _mqtt_client_secure->beforeConnected(); + } + #endif + + return true; + +} + +bool _mqttConnectSyncClient(bool secure = false) { + bool result = false; + + #if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT + _mqtt.begin(_mqtt_server.c_str(), _mqtt_port, (secure ? _mqtt_client_secure->get() : _mqtt_client)); + _mqtt.setWill(_mqtt_will.c_str(), _mqtt_payload_offline.c_str(), _mqtt_qos, _mqtt_retain); + result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_user.c_str(), _mqtt_pass.c_str()); + #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT + _mqtt.setClient(secure ? _mqtt_client_secure->get() : _mqtt_client); + _mqtt.setServer(_mqtt_server.c_str(), _mqtt_port); + + if (_mqtt_user.length() && _mqtt_pass.length()) { + DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user.c_str()); + result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_user.c_str(), _mqtt_pass.c_str(), _mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str()); + } else { + result = _mqtt.connect(_mqtt_clientid.c_str(), _mqtt_will.c_str(), _mqtt_qos, _mqtt_retain, _mqtt_payload_offline.c_str()); + } + #endif + + #if SECURE_CLIENT != SECURE_CLIENT_NONE + if (result && secure) { + result = _mqtt_client_secure->afterConnected(); + } + #endif + + return result; +} + +#endif // (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) + + void _mqttConnect() { // Do not connect if disabled @@ -82,179 +218,155 @@ void _mqttConnect() { _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX; } - String h = getSetting("mqttServer", MQTT_SERVER); #if MDNS_CLIENT_SUPPORT - h = mdnsResolve(h); + _mqtt_server = mdnsResolve(_mqtt_server); #endif - char * host = strdup(h.c_str()); - - unsigned int port = getSetting("mqttPort", MQTT_PORT).toInt(); - - if (_mqtt_user) free(_mqtt_user); - if (_mqtt_pass) free(_mqtt_pass); - if (_mqtt_will) free(_mqtt_will); - if (_mqtt_clientid) free(_mqtt_clientid); - - String user = getSetting("mqttUser", MQTT_USER); - _mqttPlaceholders(&user); - _mqtt_user = strdup(user.c_str()); - _mqtt_pass = strdup(getSetting("mqttPassword", MQTT_PASS).c_str()); - _mqtt_will = strdup(mqttTopic(MQTT_TOPIC_STATUS, false).c_str()); - String clientid = getSetting("mqttClientID", getIdentifier()); - _mqttPlaceholders(&clientid); - _mqtt_clientid = strdup(clientid.c_str()); - - DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%d\n"), host, port); - - #if MQTT_USE_ASYNC - _mqtt_connecting = true; - - _mqtt.setServer(host, port); - _mqtt.setClientId(_mqtt_clientid); - _mqtt.setKeepAlive(_mqtt_keepalive); - _mqtt.setCleanSession(false); - _mqtt.setWill(_mqtt_will, _mqtt_qos, _mqtt_retain, "0"); - if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) { - DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user); - _mqtt.setCredentials(_mqtt_user, _mqtt_pass); - } - #if ASYNC_TCP_SSL_ENABLED + DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%u\n"), _mqtt_server.c_str(), _mqtt_port); - bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; - _mqtt.setSecure(secure); - if (secure) { - DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); - unsigned char fp[20] = {0}; - if (sslFingerPrintArray(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) { - _mqtt.addServerFingerprint(fp); - } else { - DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n")); - } - } + DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid.c_str()); + DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos); + DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0); + DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive); + DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will.c_str()); - #endif // ASYNC_TCP_SSL_ENABLED + _mqtt_connecting = true; - DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid); - DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos); - DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0); - DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive); - DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will); + #if SECURE_CLIENT != SECURE_CLIENT_NONE + const bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; + #else + const bool secure = false; + #endif - _mqtt.connect(); + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + _mqttSetupAsyncClient(secure); + #elif (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) + if (_mqttSetupSyncClient(secure) && _mqttConnectSyncClient(secure)) { + _mqttOnConnect(); + } else { + DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n")); + _mqttOnDisconnect(); + } + #else + #error "please check that MQTT_LIBRARY is valid" + #endif - #else // not MQTT_USE_ASYNC +} - bool response = true; +void _mqttPlaceholders(String& text) { - #if ASYNC_TCP_SSL_ENABLED + text.replace("{hostname}", getSetting("hostname")); + text.replace("{magnitude}", "#"); - bool secure = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; - if (secure) { - DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); - if (_mqtt_client_secure.connect(host, port)) { - char fp[60] = {0}; - if (sslFingerPrintChar(getSetting("mqttFP", MQTT_SSL_FINGERPRINT).c_str(), fp)) { - if (_mqtt_client_secure.verify(fp, host)) { - _mqtt.setClient(_mqtt_client_secure); - } else { - DEBUG_MSG_P(PSTR("[MQTT] Invalid fingerprint\n")); - response = false; - } - _mqtt_client_secure.stop(); - yield(); - } else { - DEBUG_MSG_P(PSTR("[MQTT] Wrong fingerprint\n")); - response = false; - } - } else { - DEBUG_MSG_P(PSTR("[MQTT] Client connection failed\n")); - response = false; - } + String mac = WiFi.macAddress(); + mac.replace(":", ""); + text.replace("{mac}", mac); - } else { - _mqtt.setClient(_mqtt_client); - } +} - #else // not ASYNC_TCP_SSL_ENABLED +template +void _mqttApplySetting(T& current, T& updated) { + if (current != updated) { + current = std::move(updated); + mqttDisconnect(); + } +} - _mqtt.setClient(_mqtt_client); +template +void _mqttApplySetting(T& current, const T& updated) { + if (current != updated) { + current = updated; + mqttDisconnect(); + } +} - #endif // ASYNC_TCP_SSL_ENABLED +template +void _mqttApplyTopic(T& current, const char* magnitude) { + String updated = mqttTopic(magnitude, false); + if (current != updated) { + mqttFlush(); + current = std::move(updated); + } +} - if (response) { +void _mqttConfigure() { - _mqtt.setServer(host, port); + // Enable only when server is set + { + String server = getSetting("mqttServer", MQTT_SERVER); + uint16_t port = getSetting("mqttPort", MQTT_PORT).toInt(); + bool enabled = false; + if (server.length()) { + enabled = getSetting("mqttEnabled", MQTT_ENABLED).toInt() == 1; + } - if ((strlen(_mqtt_user) > 0) && (strlen(_mqtt_pass) > 0)) { - DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_user); - response = _mqtt.connect(_mqtt_clientid, _mqtt_user, _mqtt_pass, _mqtt_will, _mqtt_qos, _mqtt_retain, "0"); - } else { - response = _mqtt.connect(_mqtt_clientid, _mqtt_will, _mqtt_qos, _mqtt_retain, "0"); - } + _mqttApplySetting(_mqtt_server, server); + _mqttApplySetting(_mqtt_enabled, enabled); + _mqttApplySetting(_mqtt_port, port); - DEBUG_MSG_P(PSTR("[MQTT] Client ID: %s\n"), _mqtt_clientid); - DEBUG_MSG_P(PSTR("[MQTT] QoS: %d\n"), _mqtt_qos); - DEBUG_MSG_P(PSTR("[MQTT] Retain flag: %d\n"), _mqtt_retain ? 1 : 0); - DEBUG_MSG_P(PSTR("[MQTT] Keepalive time: %ds\n"), _mqtt_keepalive); - DEBUG_MSG_P(PSTR("[MQTT] Will topic: %s\n"), _mqtt_will); + if (!enabled) return; + } - } + // Get base topic and apply placeholders + { + String topic = getSetting("mqttTopic", MQTT_TOPIC); + if (topic.endsWith("/")) topic.remove(topic.length()-1); - if (response) { - _mqttOnConnect(); - } else { - DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n")); - _mqtt_last_connection = millis(); - } + // Replace things inside curly braces (like {hostname}, {mac} etc.) + _mqttPlaceholders(topic); - #endif // MQTT_USE_ASYNC + if (topic.indexOf("#") == -1) topic.concat("/#"); + _mqttApplySetting(_mqtt_topic, topic); - free(host); + _mqttApplyTopic(_mqtt_will, MQTT_TOPIC_STATUS); + } -} + // Getter and setter + { + String setter = getSetting("mqttSetter", MQTT_SETTER); + String getter = getSetting("mqttGetter", MQTT_GETTER); + bool forward = !setter.equals(getter) && RELAY_REPORT_STATUS; -void _mqttPlaceholders(String *text) { - - text->replace("{hostname}", getSetting("hostname")); - text->replace("{magnitude}", "#"); - - String mac = WiFi.macAddress(); - mac.replace(":", ""); - text->replace("{mac}", mac); + _mqttApplySetting(_mqtt_setter, setter); + _mqttApplySetting(_mqtt_getter, getter); + _mqttApplySetting(_mqtt_forward, forward); + } -} + // MQTT options + { + String user = getSetting("mqttUser", MQTT_USER); + _mqttPlaceholders(user); -void _mqttConfigure() { + String pass = getSetting("mqttPassword", MQTT_PASS); - // Get base topic - _mqtt_topic = getSetting("mqttTopic", MQTT_TOPIC); - if (_mqtt_topic.endsWith("/")) _mqtt_topic.remove(_mqtt_topic.length()-1); + unsigned char qos = getSetting("mqttQoS", MQTT_QOS).toInt(); + bool retain = getSetting("mqttRetain", MQTT_RETAIN).toInt() == 1; + unsigned long keepalive = getSetting("mqttKeep", MQTT_KEEPALIVE).toInt(); - // Placeholders - _mqttPlaceholders(&_mqtt_topic); - if (_mqtt_topic.indexOf("#") == -1) _mqtt_topic = _mqtt_topic + "/#"; + String id = getSetting("mqttClientID", getIdentifier()); + _mqttPlaceholders(id); - // Getters and setters - _mqtt_setter = getSetting("mqttSetter", MQTT_SETTER); - _mqtt_getter = getSetting("mqttGetter", MQTT_GETTER); - _mqtt_forward = !_mqtt_getter.equals(_mqtt_setter) && RELAY_REPORT_STATUS; + _mqttApplySetting(_mqtt_user, user); + _mqttApplySetting(_mqtt_pass, pass); + _mqttApplySetting(_mqtt_qos, qos); + _mqttApplySetting(_mqtt_retain, retain); + _mqttApplySetting(_mqtt_keepalive, keepalive); + _mqttApplySetting(_mqtt_clientid, id); + } - // MQTT options - _mqtt_qos = getSetting("mqttQoS", MQTT_QOS).toInt(); - _mqtt_retain = getSetting("mqttRetain", MQTT_RETAIN).toInt() == 1; - _mqtt_keepalive = getSetting("mqttKeep", MQTT_KEEPALIVE).toInt(); - if (getSetting("mqttClientID").length() == 0) delSetting("mqttClientID"); - - // Enable - if (getSetting("mqttServer", MQTT_SERVER).length() == 0) { - mqttEnabled(false); - } else { - _mqtt_enabled = getSetting("mqttEnabled", MQTT_ENABLED).toInt() == 1; + // MQTT JSON + { + _mqttApplySetting(_mqtt_use_json, getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1); + _mqttApplyTopic(_mqtt_topic_json, MQTT_TOPIC_JSON); } - _mqtt_use_json = (getSetting("mqttUseJson", MQTT_USE_JSON).toInt() == 1); - mqttQueueTopic(MQTT_TOPIC_JSON); + // Custom payload strings + settingsProcessConfig({ + {_mqtt_payload_online, "mqttPayloadOnline", MQTT_STATUS_ONLINE}, + {_mqtt_payload_offline, "mqttPayloadOffline", MQTT_STATUS_OFFLINE} + }); + + // Reset reconnect delay to reconnect sooner _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; } @@ -267,19 +379,65 @@ void _mqttBackwards() { } } +void _mqttInfo() { + DEBUG_MSG_P(PSTR( + "[MQTT] " + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + "AsyncMqttClient" + #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT + "Arduino-MQTT" + #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT + "PubSubClient" + #endif + ", SSL " + #if SECURE_CLIENT != SEURE_CLIENT_NONE + "ENABLED" + #else + "DISABLED" + #endif + ", Autoconnect " + #if MQTT_AUTOCONNECT + "ENABLED" + #else + "DISABLED" + #endif + "\n" + )); + DEBUG_MSG_P(PSTR("[MQTT] Client %s, %s\n"), + _mqtt_enabled ? "ENABLED" : "DISABLED", + _mqtt.connected() ? "CONNECTED" : "DISCONNECTED" + ); + DEBUG_MSG_P(PSTR("[MQTT] Retry %s (Now %u, Last %u, Delay %u, Step %u)\n"), + _mqtt_connecting ? "CONNECTING" : "WAITING", + millis(), + _mqtt_last_connection, + _mqtt_reconnect_delay, + MQTT_RECONNECT_DELAY_STEP + ); +} + // ----------------------------------------------------------------------------- // WEB // ----------------------------------------------------------------------------- #if WEB_SUPPORT -bool _mqttWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _mqttWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "mqtt", 3) == 0); } -void _mqttWebSocketOnSend(JsonObject& root) { +void _mqttWebSocketOnVisible(JsonObject& root) { root["mqttVisible"] = 1; + #if ASYNC_TCP_SSL_ENABLED + root["mqttsslVisible"] = 1; + #endif +} + +void _mqttWebSocketOnData(JsonObject& root) { root["mqttStatus"] = mqttConnected(); +} + +void _mqttWebSocketOnConnected(JsonObject& root) { root["mqttEnabled"] = mqttEnabled(); root["mqttServer"] = getSetting("mqttServer", MQTT_SERVER); root["mqttPort"] = getSetting("mqttPort", MQTT_PORT); @@ -289,8 +447,7 @@ void _mqttWebSocketOnSend(JsonObject& root) { root["mqttKeep"] = _mqtt_keepalive; root["mqttRetain"] = _mqtt_retain; root["mqttQoS"] = _mqtt_qos; - #if ASYNC_TCP_SSL_ENABLED - root["mqttsslVisible"] = 1; + #if SECURE_CLIENT != SECURE_CLIENT_NONE root["mqttUseSSL"] = getSetting("mqttUseSSL", MQTT_SSL_ENABLED).toInt() == 1; root["mqttFP"] = getSetting("mqttFP", MQTT_SSL_FINGERPRINT); #endif @@ -314,6 +471,11 @@ void _mqttInitCommands() { terminalOK(); }); + terminalRegisterCommand(F("MQTT.INFO"), [](Embedis* e) { + _mqttInfo(); + terminalOK(); + }); + } #endif // TERMINAL_SUPPORT @@ -356,6 +518,8 @@ void _mqttOnConnect() { _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN; _mqtt_last_connection = millis(); + _mqtt_connecting = false; + _mqtt_connected = true; // Clean subscriptions mqttUnsubscribeRaw("#"); @@ -372,6 +536,7 @@ void _mqttOnDisconnect() { // Reset reconnection delay _mqtt_last_connection = millis(); _mqtt_connecting = false; + _mqtt_connected = false; DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); @@ -469,10 +634,13 @@ String mqttTopic(const char * magnitude, unsigned int index, bool is_set) { void mqttSendRaw(const char * topic, const char * message, bool retain) { if (_mqtt.connected()) { - #if MQTT_USE_ASYNC + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT unsigned int packetId = _mqtt.publish(topic, _mqtt_qos, retain, message); DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s (PID %d)\n"), topic, message, packetId); - #else + #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT + _mqtt.publish(topic, message, retain, _mqtt_qos); + DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message); + #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT _mqtt.publish(topic, message, retain); DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s\n"), topic, message); #endif @@ -491,9 +659,6 @@ void mqttSend(const char * topic, const char * message, bool force, bool retain) // Equeue message if (useJson) { - // Set default queue topic - mqttQueueTopic(MQTT_TOPIC_JSON); - // Enqueue new message mqttEnqueue(topic, message); @@ -568,9 +733,9 @@ void mqttFlush() { if (_mqtt_queue.size() == 0) return; // Build tree recursively - DynamicJsonBuffer jsonBuffer; + DynamicJsonBuffer jsonBuffer(1024); JsonObject& root = jsonBuffer.createObject(); - _mqttBuildTree(root, 255); + _mqttBuildTree(root, mqtt_message_t::END); // Add extra propeties #if NTP_SUPPORT && MQTT_ENQUEUE_DATETIME @@ -608,14 +773,6 @@ void mqttFlush() { } -void mqttQueueTopic(const char * topic) { - String t = mqttTopic(topic, false); - if (!t.equals(_mqtt_topic_json)) { - mqttFlush(); - _mqtt_topic_json = t; - } -} - int8_t mqttEnqueue(const char * topic, const char * message, unsigned char parent) { // Queue is not meant to send message "offline" @@ -641,17 +798,17 @@ int8_t mqttEnqueue(const char * topic, const char * message, unsigned char paren } int8_t mqttEnqueue(const char * topic, const char * message) { - return mqttEnqueue(topic, message, 255); + return mqttEnqueue(topic, message, mqtt_message_t::END); } // ----------------------------------------------------------------------------- void mqttSubscribeRaw(const char * topic) { if (_mqtt.connected() && (strlen(topic) > 0)) { - #if MQTT_USE_ASYNC + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT unsigned int packetId = _mqtt.subscribe(topic, _mqtt_qos); DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s (PID %d)\n"), topic, packetId); - #else + #else // Arduino-MQTT or PubSubClient _mqtt.subscribe(topic, _mqtt_qos); DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s\n"), topic); #endif @@ -664,10 +821,10 @@ void mqttSubscribe(const char * topic) { void mqttUnsubscribeRaw(const char * topic) { if (_mqtt.connected() && (strlen(topic) > 0)) { - #if MQTT_USE_ASYNC + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT unsigned int packetId = _mqtt.unsubscribe(topic); DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)\n"), topic, packetId); - #else + #else // Arduino-MQTT or PubSubClient _mqtt.unsubscribe(topic); DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing to %s\n"), topic); #endif @@ -709,7 +866,11 @@ void mqttRegister(mqtt_callback_f callback) { void mqttSetBroker(IPAddress ip, unsigned int port) { setSetting("mqttServer", ip.toString()); + _mqtt_server = ip.toString(); + setSetting("mqttPort", port); + _mqtt_port = port; + mqttEnabled(MQTT_AUTOCONNECT); } @@ -717,9 +878,20 @@ void mqttSetBrokerIfNone(IPAddress ip, unsigned int port) { if (getSetting("mqttServer", MQTT_SERVER).length() == 0) mqttSetBroker(ip, port); } -void mqttReset() { - _mqttConfigure(); - mqttDisconnect(); +const String& mqttPayloadOnline() { + return _mqtt_payload_online; +} + +const String& mqttPayloadOffline() { + return _mqtt_payload_offline; +} + +const char* mqttPayloadStatus(bool status) { + return status ? _mqtt_payload_online.c_str() : _mqtt_payload_offline.c_str(); +} + +void mqttSendStatus() { + mqttSend(MQTT_TOPIC_STATUS, _mqtt_payload_online.c_str(), true); } // ----------------------------------------------------------------------------- @@ -729,14 +901,22 @@ void mqttReset() { void mqttSetup() { _mqttBackwards(); - - DEBUG_MSG_P(PSTR("[MQTT] Async %s, SSL %s, Autoconnect %s\n"), - MQTT_USE_ASYNC ? "ENABLED" : "DISABLED", - ASYNC_TCP_SSL_ENABLED ? "ENABLED" : "DISABLED", - MQTT_AUTOCONNECT ? "ENABLED" : "DISABLED" - ); - - #if MQTT_USE_ASYNC + _mqttInfo(); + + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT + + // XXX: should not place this in config, addServerFingerprint does not check for duplicates + #if SECURE_CLIENT != SECURE_CLIENT_NONE + { + if (_mqtt_sc_config.on_fingerprint) { + const String fingerprint = _mqtt_sc_config.on_fingerprint(); + uint8_t buffer[20] = {0}; + if (sslFingerPrintArray(fingerprint.c_str(), buffer)) { + _mqtt.addServerFingerprint(buffer); + } + } + } + #endif // SECURE_CLIENT != SECURE_CLIENT_NONE _mqtt.onConnect([](bool sessionPresent) { _mqttOnConnect(); @@ -757,7 +937,7 @@ void mqttSetup() { if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) { DEBUG_MSG_P(PSTR("[MQTT] Not authorized\n")); } - #if ASYNC_TCP_SSL_ENABLED + #if SECURE_CLIENT == SECURE_CLIENT_AXTLS if (reason == AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT) { DEBUG_MSG_P(PSTR("[MQTT] Bad fingerprint\n")); } @@ -774,20 +954,33 @@ void mqttSetup() { DEBUG_MSG_P(PSTR("[MQTT] Publish ACK for PID %d\n"), packetId); }); - #else // not MQTT_USE_ASYNC + #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT + + _mqtt.onMessageAdvanced([](MQTTClient *client, char topic[], char payload[], int length) { + _mqttOnMessage(topic, payload, length); + }); + + #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT _mqtt.setCallback([](char* topic, byte* payload, unsigned int length) { _mqttOnMessage(topic, (char *) payload, length); }); - #endif // MQTT_USE_ASYNC + #endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqttConfigure(); mqttRegister(_mqttCallback); #if WEB_SUPPORT - wsOnSendRegister(_mqttWebSocketOnSend); - wsOnReceiveRegister(_mqttWebSocketOnReceive); + wsRegister() + .onVisible(_mqttWebSocketOnVisible) + .onData(_mqttWebSocketOnData) + .onConnected(_mqttWebSocketOnConnected) + .onKeyCheck(_mqttWebSocketOnKeyCheck); + + mqttRegister([](unsigned int type, const char*, const char*) { + if ((type == MQTT_CONNECT_EVENT) || (type == MQTT_DISCONNECT_EVENT)) wsPost(_mqttWebSocketOnData); + }); #endif #if TERMINAL_SUPPORT @@ -804,11 +997,11 @@ void mqttLoop() { if (WiFi.status() != WL_CONNECTED) return; - #if MQTT_USE_ASYNC + #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqttConnect(); - #else // not MQTT_USE_ASYNC + #else // MQTT_LIBRARY != MQTT_LIBRARY_ASYNCMQTTCLIENT if (_mqtt.connected()) { @@ -818,14 +1011,13 @@ void mqttLoop() { if (_mqtt_connected) { _mqttOnDisconnect(); - _mqtt_connected = false; } _mqttConnect(); } - #endif + #endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT } diff --git a/code/espurna/nofuss.ino b/code/espurna/nofuss.ino index 4ec35c36..5c6568a4 100644 --- a/code/espurna/nofuss.ino +++ b/code/espurna/nofuss.ino @@ -20,12 +20,15 @@ bool _nofussEnabled = false; #if WEB_SUPPORT -bool _nofussWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _nofussWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "nofuss", 6) == 0); } -void _nofussWebSocketOnSend(JsonObject& root) { +void _nofussWebSocketOnVisible(JsonObject& root) { root["nofussVisible"] = 1; +} + +void _nofussWebSocketOnConnected(JsonObject& root) { root["nofussEnabled"] = getSetting("nofussEnabled", NOFUSS_ENABLED).toInt() == 1; root["nofussServer"] = getSetting("nofussServer", NOFUSS_SERVER); } @@ -54,16 +57,15 @@ void _nofussConfigure() { } else { - char buffer[20]; - snprintf_P(buffer, sizeof(buffer), PSTR("%s-%s"), APP_NAME, DEVICE); - NoFUSSClient.setServer(nofussServer); - NoFUSSClient.setDevice(buffer); + NoFUSSClient.setDevice(APP_NAME "_" DEVICE); NoFUSSClient.setVersion(APP_VERSION); + NoFUSSClient.setBuild(String(__UNIX_TIMESTAMP__)); DEBUG_MSG_P(PSTR("[NOFUSS] Server : %s\n"), nofussServer.c_str()); - DEBUG_MSG_P(PSTR("[NOFUSS] Dervice: %s\n"), buffer); + DEBUG_MSG_P(PSTR("[NOFUSS] Dervice: %s\n"), APP_NAME "_" DEVICE); DEBUG_MSG_P(PSTR("[NOFUSS] Version: %s\n"), APP_VERSION); + DEBUG_MSG_P(PSTR("[NOFUSS] Build: %s\n"), String(__UNIX_TIMESTAMP__).c_str()); DEBUG_MSG_P(PSTR("[NOFUSS] Enabled\n")); } @@ -123,6 +125,9 @@ void nofussSetup() { // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade eepromRotate(false); + + // Force backup right now, because NoFUSS library will immediatly reset on success + eepromBackup(0); } if (code == NOFUSS_FILESYSTEM_UPDATE_ERROR) { @@ -146,6 +151,8 @@ void nofussSetup() { #if WEB_SUPPORT wsSend_P(PSTR("{\"action\": \"reload\"}")); #endif + // TODO: NoFUSS will reset the board after this callback returns. + // Maybe this should be optional nice_delay(100); } @@ -157,8 +164,10 @@ void nofussSetup() { }); #if WEB_SUPPORT - wsOnSendRegister(_nofussWebSocketOnSend); - wsOnReceiveRegister(_nofussWebSocketOnReceive); + wsRegister() + .onVisible(_nofussWebSocketOnVisible) + .onConnected(_nofussWebSocketOnConnected) + .onKeyCheck(_nofussWebSocketOnKeyCheck); #endif #if TERMINAL_SUPPORT diff --git a/code/espurna/ntp.ino b/code/espurna/ntp.ino index 103ec6b7..91a2567e 100644 --- a/code/espurna/ntp.ino +++ b/code/espurna/ntp.ino @@ -26,13 +26,19 @@ bool _ntp_want_sync = false; #if WEB_SUPPORT -bool _ntpWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _ntpWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "ntp", 3) == 0); } -void _ntpWebSocketOnSend(JsonObject& root) { +void _ntpWebSocketOnVisible(JsonObject& root) { root["ntpVisible"] = 1; +} + +void _ntpWebSocketOnData(JsonObject& root) { root["ntpStatus"] = (timeStatus() == timeSet); +} + +void _ntpWebSocketOnConnected(JsonObject& root) { root["ntpServer"] = getSetting("ntpServer", NTP_SERVER); root["ntpOffset"] = getSetting("ntpOffset", NTP_TIME_OFFSET).toInt(); root["ntpDST"] = getSetting("ntpDST", NTP_DAY_LIGHT).toInt() == 1; @@ -116,10 +122,6 @@ void _ntpReport() { _ntp_report = false; - #if WEB_SUPPORT - wsSend(_ntpWebSocketOnSend); - #endif - if (ntpSynced()) { time_t t = now(); DEBUG_MSG_P(PSTR("[NTP] UTC Time : %s\n"), ntpDateTime(ntpLocal2UTC(t)).c_str()); @@ -233,14 +235,14 @@ void ntpSetup() { NTPw.onNTPSyncEvent([](NTPSyncEvent_t error) { if (error) { - #if WEB_SUPPORT - wsSend_P(PSTR("{\"ntpStatus\": false}")); - #endif if (error == noResponse) { DEBUG_MSG_P(PSTR("[NTP] Error: NTP server not reachable\n")); } else if (error == invalidAddress) { DEBUG_MSG_P(PSTR("[NTP] Error: Invalid NTP server address\n")); } + #if WEB_SUPPORT + wsPost(_ntpWebSocketOnData); + #endif } else { _ntp_report = true; setTime(NTPw.getLastNTPSync()); @@ -256,8 +258,11 @@ void ntpSetup() { }); #if WEB_SUPPORT - wsOnSendRegister(_ntpWebSocketOnSend); - wsOnReceiveRegister(_ntpWebSocketOnReceive); + wsRegister() + .onVisible(_ntpWebSocketOnVisible) + .onConnected(_ntpWebSocketOnConnected) + .onData(_ntpWebSocketOnData) + .onKeyCheck(_ntpWebSocketOnKeyCheck); #endif // Main callbacks diff --git a/code/espurna/ota.ino b/code/espurna/ota.ino deleted file mode 100644 index 0f504adb..00000000 --- a/code/espurna/ota.ino +++ /dev/null @@ -1,295 +0,0 @@ -/* - -OTA MODULE - -Copyright (C) 2016-2019 by Xose Pérez - -*/ - -#include "ArduinoOTA.h" - -// ----------------------------------------------------------------------------- -// Arduino OTA -// ----------------------------------------------------------------------------- - -void _otaConfigure() { - ArduinoOTA.setPort(OTA_PORT); - ArduinoOTA.setHostname(getSetting("hostname").c_str()); - #if USE_PASSWORD - ArduinoOTA.setPassword(getAdminPass().c_str()); - #endif -} - -void _otaLoop() { - ArduinoOTA.handle(); -} - -// ----------------------------------------------------------------------------- -// Terminal OTA -// ----------------------------------------------------------------------------- - -#if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT - -#include -AsyncClient * _ota_client; -char * _ota_host; -char * _ota_url; -unsigned int _ota_port = 80; -unsigned long _ota_size = 0; - -const char OTA_REQUEST_TEMPLATE[] PROGMEM = - "GET %s HTTP/1.1\r\n" - "Host: %s\r\n" - "User-Agent: ESPurna\r\n" - "Connection: close\r\n" - "Content-Type: application/x-www-form-urlencoded\r\n" - "Content-Length: 0\r\n\r\n\r\n"; - - -void _otaFrom(const char * host, unsigned int port, const char * url) { - - if (_ota_host) free(_ota_host); - if (_ota_url) free(_ota_url); - _ota_host = strdup(host); - _ota_url = strdup(url); - _ota_port = port; - _ota_size = 0; - - if (_ota_client == NULL) { - _ota_client = new AsyncClient(); - } - - _ota_client->onDisconnect([](void *s, AsyncClient *c) { - - DEBUG_MSG_P(PSTR("\n")); - - if (Update.end(true)){ - DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), _ota_size); - deferredReset(100, CUSTOM_RESET_OTA); - } else { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - eepromRotate(true); - } - - DEBUG_MSG_P(PSTR("[OTA] Disconnected\n")); - - _ota_client->free(); - delete _ota_client; - _ota_client = NULL; - free(_ota_host); - _ota_host = NULL; - free(_ota_url); - _ota_url = NULL; - - }, 0); - - _ota_client->onTimeout([](void *s, AsyncClient *c, uint32_t time) { - _ota_client->close(true); - }, 0); - - _ota_client->onData([](void * arg, AsyncClient * c, void * data, size_t len) { - - char * p = (char *) data; - - if (_ota_size == 0) { - - Update.runAsync(true); - if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - } - - p = strstr((char *)data, "\r\n\r\n") + 4; - len = len - (p - (char *) data); - - } - - if (!Update.hasError()) { - if (Update.write((uint8_t *) p, len) != len) { - #ifdef DEBUG_PORT - Update.printError(DEBUG_PORT); - #endif - } - } - - _ota_size += len; - DEBUG_MSG_P(PSTR("[OTA] Progress: %u bytes\r"), _ota_size); - - delay(0); - - }, NULL); - - _ota_client->onConnect([](void * arg, AsyncClient * client) { - - #if ASYNC_TCP_SSL_ENABLED - if (443 == _ota_port) { - uint8_t fp[20] = {0}; - sslFingerPrintArray(getSetting("otafp", OTA_GITHUB_FP).c_str(), fp); - SSL * ssl = _ota_client->getSSL(); - if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { - DEBUG_MSG_P(PSTR("[OTA] Warning: certificate doesn't match\n")); - } - } - #endif - - // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade - eepromRotate(false); - - DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url); - char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + strlen(_ota_url) + strlen(_ota_host)]; - snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url, _ota_host); - client->write(buffer); - - }, NULL); - - #if ASYNC_TCP_SSL_ENABLED - bool connected = _ota_client->connect(host, port, 443 == port); - #else - bool connected = _ota_client->connect(host, port); - #endif - - if (!connected) { - DEBUG_MSG_P(PSTR("[OTA] Connection failed\n")); - _ota_client->close(true); - } - -} - -void _otaFrom(String url) { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); - return; - } - - // Port from protocol - unsigned int port = 80; - if (url.startsWith("https://")) port = 443; - url = url.substring(url.indexOf("/") + 2); - - // Get host - String host = url.substring(0, url.indexOf("/")); - - // Explicit port - int p = host.indexOf(":"); - if (p > 0) { - port = host.substring(p + 1).toInt(); - host = host.substring(0, p); - } - - // Get URL - String uri = url.substring(url.indexOf("/")); - - _otaFrom(host.c_str(), port, uri.c_str()); - -} - -#endif // TERMINAL_SUPPORT || OTA_MQTT_SUPPORT - - -#if TERMINAL_SUPPORT - -void _otaInitCommands() { - - terminalRegisterCommand(F("OTA"), [](Embedis* e) { - if (e->argc < 2) { - terminalError(F("Wrong arguments")); - } else { - terminalOK(); - String url = String(e->argv[1]); - _otaFrom(url); - } - }); - -} - -#endif // TERMINAL_SUPPORT - -#if OTA_MQTT_SUPPORT - -void _otaMQTTCallback(unsigned int type, const char * topic, const char * payload) { - if (type == MQTT_CONNECT_EVENT) { - mqttSubscribe(MQTT_TOPIC_OTA); - } - - if (type == MQTT_MESSAGE_EVENT) { - // Match topic - String t = mqttMagnitude((char *) topic); - if (t.equals(MQTT_TOPIC_OTA)) { - DEBUG_MSG_P(PSTR("[OTA] Initiating from URL: %s\n"), payload); - _otaFrom(payload); - } - } -} - -#endif // OTA_MQTT_SUPPORT - -// ----------------------------------------------------------------------------- - -void otaSetup() { - - _otaConfigure(); - - #if TERMINAL_SUPPORT - _otaInitCommands(); - #endif - - #if OTA_MQTT_SUPPORT - mqttRegister(_otaMQTTCallback); - #endif - - // Main callbacks - espurnaRegisterLoop(_otaLoop); - espurnaRegisterReload(_otaConfigure); - - // ------------------------------------------------------------------------- - - ArduinoOTA.onStart([]() { - - // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade - eepromRotate(false); - - DEBUG_MSG_P(PSTR("[OTA] Start\n")); - - #if WEB_SUPPORT - wsSend_P(PSTR("{\"message\": 2}")); - #endif - - }); - - ArduinoOTA.onEnd([]() { - DEBUG_MSG_P(PSTR("\n")); - DEBUG_MSG_P(PSTR("[OTA] Done, restarting...\n")); - #if WEB_SUPPORT - wsSend_P(PSTR("{\"action\": \"reload\"}")); - #endif - deferredReset(100, CUSTOM_RESET_OTA); - }); - - ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { - static unsigned int _progOld; - - unsigned int _prog = (progress / (total / 100)); - if (_prog != _progOld) { - DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog); - _progOld = _prog; - } - }); - - ArduinoOTA.onError([](ota_error_t error) { - #if DEBUG_SUPPORT - DEBUG_MSG_P(PSTR("\n[OTA] Error #%u: "), error); - if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n")); - else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n")); - else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n")); - else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n")); - else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n")); - #endif - eepromRotate(true); - }); - - ArduinoOTA.begin(); - -} diff --git a/code/espurna/ota_arduinoota.ino b/code/espurna/ota_arduinoota.ino new file mode 100644 index 00000000..78664421 --- /dev/null +++ b/code/espurna/ota_arduinoota.ino @@ -0,0 +1,101 @@ +/* + +ARDUINO OTA MODULE + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#if OTA_ARDUINOOTA_SUPPORT + +// TODO: allocate ArduinoOTAClass on-demand, stop using global instance + +void _arduinoOtaConfigure() { + + ArduinoOTA.setPort(OTA_PORT); + ArduinoOTA.setHostname(getSetting("hostname").c_str()); + #if USE_PASSWORD + ArduinoOTA.setPassword(getAdminPass().c_str()); + #endif + ArduinoOTA.begin(); + +} + +void _arduinoOtaLoop() { + ArduinoOTA.handle(); +} + +void _arduinoOtaOnStart() { + + // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade + eepromRotate(false); + + // Because ArduinoOTA is synchronous, force backup right now instead of waiting for the next loop() + eepromBackup(0); + + DEBUG_MSG_P(PSTR("[OTA] Start\n")); + + #if WEB_SUPPORT + wsSend_P(PSTR("{\"message\": 2}")); + #endif + +} + +void _arduinoOtaOnEnd() { + + DEBUG_MSG_P(PSTR("\n")); + DEBUG_MSG_P(PSTR("[OTA] Done, restarting...\n")); + #if WEB_SUPPORT + wsSend_P(PSTR("{\"action\": \"reload\"}")); + #endif + deferredReset(100, CUSTOM_RESET_OTA); + +} + +void _arduinoOtaOnProgress(unsigned int progress, unsigned int total) { + + // Removed to avoid websocket ping back during upgrade (see #1574) + // TODO: implement as separate from debugging message + #if WEB_SUPPORT + if (wsConnected()) return; + #endif + + static unsigned int _progOld; + + unsigned int _prog = (progress / (total / 100)); + if (_prog != _progOld) { + DEBUG_MSG_P(PSTR("[OTA] Progress: %u%%\r"), _prog); + _progOld = _prog; + } + +} + +void _arduinoOtaOnError(ota_error_t error) { + + #if DEBUG_SUPPORT + DEBUG_MSG_P(PSTR("\n[OTA] Error #%u: "), error); + if (error == OTA_AUTH_ERROR) DEBUG_MSG_P(PSTR("Auth Failed\n")); + else if (error == OTA_BEGIN_ERROR) DEBUG_MSG_P(PSTR("Begin Failed\n")); + else if (error == OTA_CONNECT_ERROR) DEBUG_MSG_P(PSTR("Connect Failed\n")); + else if (error == OTA_RECEIVE_ERROR) DEBUG_MSG_P(PSTR("Receive Failed\n")); + else if (error == OTA_END_ERROR) DEBUG_MSG_P(PSTR("End Failed\n")); + #endif + eepromRotate(true); + +} + +void arduinoOtaSetup() { + + espurnaRegisterLoop(_arduinoOtaLoop); + espurnaRegisterReload(_arduinoOtaConfigure); + + ArduinoOTA.onStart(_arduinoOtaOnStart); + ArduinoOTA.onEnd(_arduinoOtaOnEnd); + ArduinoOTA.onError(_arduinoOtaOnError); + ArduinoOTA.onProgress(_arduinoOtaOnProgress); + + _arduinoOtaConfigure(); + +} + +#endif // OTA_ARDUINOOTA_SUPPORT diff --git a/code/espurna/ota_asynctcp.ino b/code/espurna/ota_asynctcp.ino new file mode 100644 index 00000000..e01ca236 --- /dev/null +++ b/code/espurna/ota_asynctcp.ino @@ -0,0 +1,227 @@ +/* + +ASYNC CLIENT OTA MODULE + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#if OTA_CLIENT == OTA_CLIENT_ASYNCTCP + +// ----------------------------------------------------------------------------- +// Terminal OTA command +// ----------------------------------------------------------------------------- + +#if TERMINAL_SUPPORT || OTA_MQTT_SUPPORT + +#include +#include "libs/URL.h" + +std::unique_ptr _ota_client = nullptr; +unsigned long _ota_size = 0; +bool _ota_connected = false; +std::unique_ptr _ota_url = nullptr; + +const char OTA_REQUEST_TEMPLATE[] PROGMEM = + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "User-Agent: ESPurna\r\n" + "Connection: close\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: 0\r\n\r\n\r\n"; + +void _otaClientOnDisconnect(void *s, AsyncClient *c) { + + DEBUG_MSG_P(PSTR("\n")); + + if (Update.end(true)){ + DEBUG_MSG_P(PSTR("[OTA] Success: %u bytes\n"), _ota_size); + deferredReset(100, CUSTOM_RESET_OTA); + } else { + #ifdef DEBUG_PORT + Update.printError(DEBUG_PORT); + #endif + eepromRotate(true); + } + + DEBUG_MSG_P(PSTR("[OTA] Disconnected\n")); + + _ota_connected = false; + _ota_url = nullptr; + _ota_client = nullptr; + +} + +void _otaClientOnTimeout(void *s, AsyncClient *c, uint32_t time) { + _ota_connected = false; + _ota_url = nullptr; + _ota_client->close(true); +} + +void _otaClientOnData(void * arg, AsyncClient * c, void * data, size_t len) { + + char * p = (char *) data; + + if (_ota_size == 0) { + + Update.runAsync(true); + if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) { + #ifdef DEBUG_PORT + Update.printError(DEBUG_PORT); + #endif + c->close(true); + return; + } + + p = strstr((char *)data, "\r\n\r\n") + 4; + len = len - (p - (char *) data); + + } + + if (!Update.hasError()) { + if (Update.write((uint8_t *) p, len) != len) { + #ifdef DEBUG_PORT + Update.printError(DEBUG_PORT); + #endif + c->close(true); + return; + } + } + + _ota_size += len; + DEBUG_MSG_P(PSTR("[OTA] Progress: %u bytes\r"), _ota_size); + + delay(0); + +} + +void _otaClientOnConnect(void *arg, AsyncClient *client) { + + #if ASYNC_TCP_SSL_ENABLED + int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt(); + if ((check == SECURE_CLIENT_CHECK_FINGERPRINT) && (443 == _ota_url->port)) { + uint8_t fp[20] = {0}; + sslFingerPrintArray(getSetting("otafp", OTA_FINGERPRINT).c_str(), fp); + SSL * ssl = _ota_client->getSSL(); + if (ssl_match_fingerprint(ssl, fp) != SSL_OK) { + DEBUG_MSG_P(PSTR("[OTA] Warning: certificate fingerpint doesn't match\n")); + client->close(true); + return; + } + } + #endif + + // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade + eepromRotate(false); + + DEBUG_MSG_P(PSTR("[OTA] Downloading %s\n"), _ota_url->path.c_str()); + char buffer[strlen_P(OTA_REQUEST_TEMPLATE) + _ota_url->path.length() + _ota_url->host.length()]; + snprintf_P(buffer, sizeof(buffer), OTA_REQUEST_TEMPLATE, _ota_url->path.c_str(), _ota_url->host.c_str()); + client->write(buffer); +} + +void _otaClientFrom(const String& url) { + + if (_ota_connected) { + DEBUG_MSG_P(PSTR("[OTA] Already connected\n")); + return; + } + + _ota_size = 0; + + if (_ota_url) _ota_url = nullptr; + _ota_url = std::make_unique(url); + /* + DEBUG_MSG_P(PSTR("[OTA] proto:%s host:%s port:%u path:%s\n"), + _ota_url->protocol.c_str(), + _ota_url->host.c_str(), + _ota_url->port, + _ota_url->path.c_str() + ); + */ + + // we only support HTTP + if ((!_ota_url->protocol.equals("http")) && (!_ota_url->protocol.equals("https"))) { + DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); + _ota_url = nullptr; + return; + } + + if (!_ota_client) { + _ota_client = std::make_unique(); + } + + _ota_client->onDisconnect(_otaClientOnDisconnect, nullptr); + _ota_client->onTimeout(_otaClientOnTimeout, nullptr); + _ota_client->onData(_otaClientOnData, nullptr); + _ota_client->onConnect(_otaClientOnConnect, nullptr); + + #if ASYNC_TCP_SSL_ENABLED + _ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port, 443 == _ota_url->port); + #else + _ota_connected = _ota_client->connect(_ota_url->host.c_str(), _ota_url->port); + #endif + + if (!_ota_connected) { + DEBUG_MSG_P(PSTR("[OTA] Connection failed\n")); + _ota_url = nullptr; + _ota_client->close(true); + } + +} + +#endif // TERMINAL_SUPPORT || OTA_MQTT_SUPPORT + + +#if TERMINAL_SUPPORT + +void _otaClientInitCommands() { + + terminalRegisterCommand(F("OTA"), [](Embedis* e) { + if (e->argc < 2) { + terminalError(F("OTA ")); + } else { + _otaClientFrom(String(e->argv[1])); + terminalOK(); + } + }); + +} + +#endif // TERMINAL_SUPPORT + +#if OTA_MQTT_SUPPORT + +void _otaClientMqttCallback(unsigned int type, const char * topic, const char * payload) { + + if (type == MQTT_CONNECT_EVENT) { + mqttSubscribe(MQTT_TOPIC_OTA); + } + + if (type == MQTT_MESSAGE_EVENT) { + String t = mqttMagnitude((char *) topic); + if (t.equals(MQTT_TOPIC_OTA)) { + DEBUG_MSG_P(PSTR("[OTA] Initiating from URL: %s\n"), payload); + _otaClientFrom(payload); + } + } + +} + +#endif // OTA_MQTT_SUPPORT + +// ----------------------------------------------------------------------------- + +void otaClientSetup() { + + #if TERMINAL_SUPPORT + _otaClientInitCommands(); + #endif + + #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) + mqttRegister(_otaClientMqttCallback); + #endif + +} + +#endif // OTA_CLIENT == OTA_CLIENT_ASYNCTCP diff --git a/code/espurna/ota_httpupdate.ino b/code/espurna/ota_httpupdate.ino new file mode 100644 index 00000000..6cb2b49a --- /dev/null +++ b/code/espurna/ota_httpupdate.ino @@ -0,0 +1,264 @@ +/* + +HTTP(s) OTA MODULE + +Copyright (C) 2019 by Maxim Prokhorov + +*/ + +// ----------------------------------------------------------------------------- +// OTA by using Core's HTTP(s) updater +// ----------------------------------------------------------------------------- + +#if OTA_CLIENT == OTA_CLIENT_HTTPUPDATE + +#include + +#include +#include + +#include "libs/URL.h" + +#if SECURE_CLIENT != SECURE_CLIENT_NONE + + #if OTA_SECURE_CLIENT_INCLUDE_CA + #include "static/ota_client_trusted_root_ca.h" + #else + #include "static/digicert_evroot_pem.h" + #define _ota_client_trusted_root_ca _ssl_digicert_ev_root_ca + #endif + +#endif // SECURE_CLIENT != SECURE_CLIENT_NONE + + +void _otaClientRunUpdater(WiFiClient* client, const String& url, const String& fp = "") { + + UNUSED(client); + UNUSED(fp); + + // Disabling EEPROM rotation to prevent writing to EEPROM after the upgrade + eepromRotate(false); + + DEBUG_MSG_P(PSTR("[OTA] Downloading %s ...\n"), url.c_str()); + + // TODO: support currentVersion (string arg after 'url') + // NOTE: ESPhttpUpdate.update(..., fp) will **always** fail with empty fingerprint + // NOTE: It is possible to support BearSSL with 2.4.2 by using uint8_t[20] instead of String for fingerprint argument + + ESPhttpUpdate.rebootOnUpdate(false); + t_httpUpdate_return result = HTTP_UPDATE_NO_UPDATES; + + // We expect both .update(url, "", String_fp) and .update(url) to survice until axTLS is removed from the Core + #if (SECURE_CLIENT == SECURE_CLIENT_AXTLS) + if (url.startsWith("https://")) { + result = ESPhttpUpdate.update(url, "", fp); + } else { + result = ESPhttpUpdate.update(url); + } + #elif OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE + result = ESPhttpUpdate.update(url); + #else + result = ESPhttpUpdate.update(*client, url); + #endif + + switch (result) { + case HTTP_UPDATE_FAILED: + DEBUG_MSG_P(PSTR("[OTA] Update failed (error %d): %s\n"), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); + eepromRotate(true); + break; + case HTTP_UPDATE_NO_UPDATES: + DEBUG_MSG_P(PSTR("[OTA] No updates")); + eepromRotate(true); + break; + case HTTP_UPDATE_OK: + DEBUG_MSG_P(PSTR("[OTA] Done, restarting...")); + deferredReset(500, CUSTOM_RESET_OTA); // wait a bit more than usual + break; + } + +} + +#if OTA_CLIENT_HTTPUPDATE_2_3_0_COMPATIBLE +void _otaClientFromHttp(const String& url) { + _otaClientRunUpdater(nullptr, url, ""); +} +#else +void _otaClientFromHttp(const String& url) { + auto client = std::make_unique(); + _otaClientRunUpdater(client.get(), url, ""); +} +#endif + +#if SECURE_CLIENT == SECURE_CLIENT_BEARSSL + +void _otaClientFromHttps(const String& url) { + + int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt(); + bool settime = (check == SECURE_CLIENT_CHECK_CA); + + if (!ntpSynced() && settime) { + DEBUG_MSG_P(PSTR("[OTA] Time not synced!\n")); + return; + } + + // unique_ptr self-destructs after exiting function scope + // create WiFiClient on heap to use less stack space + auto client = std::make_unique(); + + if (check == SECURE_CLIENT_CHECK_NONE) { + DEBUG_MSG_P(PSTR("[OTA] !!! Connection will not be validated !!!\n")); + client->setInsecure(); + } + + if (check == SECURE_CLIENT_CHECK_FINGERPRINT) { + String fp_string = getSetting("otafp", OTA_FINGERPRINT); + if (!fp_string.length()) { + DEBUG_MSG_P(PSTR("[OTA] Requested fingerprint auth, but 'otafp' is not set\n")); + return; + } + + uint8_t fp_bytes[20] = {0}; + sslFingerPrintArray(fp_string.c_str(), fp_bytes); + + client->setFingerprint(fp_bytes); + } + + BearSSL::X509List *ca = nullptr; + if (check == SECURE_CLIENT_CHECK_CA) { + ca = new BearSSL::X509List(_ota_client_trusted_root_ca); + // because we do not support libc methods of getting time, force client to use ntpclientlib's current time + // XXX: local2utc method use is detrimental when DST happening. now() should be utc + client->setX509Time(ntpLocal2UTC(now())); + client->setTrustAnchors(ca); + } + + // TODO: RX and TX buffer sizes must be equal? + const uint16_t requested_mfln = getSetting("otaScMFLN", OTA_SECURE_CLIENT_MFLN).toInt(); + switch (requested_mfln) { + // default, do nothing + case 0: + break; + // match valid sizes only + case 512: + case 1024: + case 2048: + case 4096: + { + client->setBufferSizes(requested_mfln, requested_mfln); + break; + } + default: + DEBUG_MSG_P(PSTR("[OTA] Warning: MFLN buffer size must be one of 512, 1024, 2048 or 4096\n")); + } + + _otaClientRunUpdater(client.get(), url); + +} + +#endif // SECURE_CLIENT_BEARSSL + + +#if SECURE_CLIENT == SECURE_CLIENT_AXTLS + +void _otaClientFromHttps(const String& url) { + + const int check = getSetting("otaScCheck", OTA_SECURE_CLIENT_CHECK).toInt(); + + String fp_string; + if (check == SECURE_CLIENT_CHECK_FINGERPRINT) { + fp_string = getSetting("otafp", OTA_FINGERPRINT); + if (!fp_string.length() || !sslCheckFingerPrint(fp_string.c_str())) { + DEBUG_MSG_P(PSTR("[OTA] Wrong fingerprint\n")); + return; + } + } + + _otaClientRunUpdater(nullptr, url, fp_string); + +} + +#endif // SECURE_CLIENT_AXTLS + +void _otaClientFrom(const String& url) { + + if (url.startsWith("http://")) { + _otaClientFromHttp(url); + return; + } + + #if SECURE_CLIENT != SECURE_CLIENT_NONE + if (url.startsWith("https://")) { + _otaClientFromHttps(url); + return; + } + #endif + + DEBUG_MSG_P(PSTR("[OTA] Incorrect URL specified\n")); + +} + +#if TERMINAL_SUPPORT + +void _otaClientInitCommands() { + + terminalRegisterCommand(F("OTA"), [](Embedis* e) { + if (e->argc < 2) { + terminalError(F("OTA ")); + } else { + _otaClientFrom(String(e->argv[1])); + terminalOK(); + } + }); + +} + +#endif // TERMINAL_SUPPORT + +#if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) + +bool _ota_do_update = false; +String _ota_url; + +void _otaClientLoop() { + if (_ota_do_update) { + _otaClientFrom(_ota_url); + _ota_do_update = false; + _ota_url = ""; + } +} + +void _otaClientMqttCallback(unsigned int type, const char * topic, const char * payload) { + + if (type == MQTT_CONNECT_EVENT) { + mqttSubscribe(MQTT_TOPIC_OTA); + } + + if (type == MQTT_MESSAGE_EVENT) { + String t = mqttMagnitude((char *) topic); + if (t.equals(MQTT_TOPIC_OTA)) { + DEBUG_MSG_P(PSTR("[OTA] Queuing from URL: %s\n"), payload); + _ota_do_update = true; + _ota_url = payload; + } + } + +} + +#endif // MQTT_SUPPORT + +// ----------------------------------------------------------------------------- + +void otaClientSetup() { + + #if TERMINAL_SUPPORT + _otaClientInitCommands(); + #endif + + #if (MQTT_SUPPORT && OTA_MQTT_SUPPORT) + mqttRegister(_otaClientMqttCallback); + espurnaRegisterLoop(_otaClientLoop); + #endif + +} + +#endif // OTA_CLIENT == OTA_CLIENT_HTTPUPDATE diff --git a/code/espurna/relay.ino b/code/espurna/relay.ino index 8f3eb5e6..298f2857 100644 --- a/code/espurna/relay.ino +++ b/code/espurna/relay.ino @@ -28,6 +28,7 @@ typedef struct { bool current_status; // Holds the current (physical) status of the relay bool target_status; // Holds the target status + unsigned char lock; // Holds the value of target status, that cannot be changed afterwards. (0 for false, 1 for true, 2 to disable) unsigned long fw_start; // Flood window start time unsigned char fw_count; // Number of changes within the current flood window unsigned long change_time; // Scheduled time to change @@ -43,6 +44,44 @@ std::vector _relays; bool _relayRecursive = false; Ticker _relaySaveTicker; +#if MQTT_SUPPORT + +String _relay_mqtt_payload_on; +String _relay_mqtt_payload_off; +String _relay_mqtt_payload_toggle; + +#endif // MQTT_SUPPORT + +// ----------------------------------------------------------------------------- +// UTILITY +// ----------------------------------------------------------------------------- + +bool _relayHandlePayload(unsigned char relayID, const char* payload) { + auto value = relayParsePayload(payload); + if (value == RelayStatus::UNKNOWN) return false; + + if (value == RelayStatus::OFF) { + relayStatus(relayID, false); + } else if (value == RelayStatus::ON) { + relayStatus(relayID, true); + } else if (value == RelayStatus::TOGGLE) { + relayToggle(relayID); + } + + return true; +} + +RelayStatus _relayStatusInvert(RelayStatus status) { + return (status == RelayStatus::ON) ? RelayStatus::OFF : status; +} + +RelayStatus _relayStatusTyped(unsigned char id) { + if (id >= _relays.size()) return RelayStatus::OFF; + + const bool status = _relays[id].current_status; + return (status) ? RelayStatus::ON : RelayStatus::OFF; +} + // ----------------------------------------------------------------------------- // RELAY PROVIDERS // ----------------------------------------------------------------------------- @@ -174,6 +213,23 @@ void _relayProcess(bool mode) { // Only process the relays we have to change to the requested mode if (target != mode) continue; + // Only process the relays that can be changed + switch (_relays[id].lock) { + case RELAY_LOCK_ON: + case RELAY_LOCK_OFF: + { + bool lock = _relays[id].lock == 1; + if (lock != _relays[id].target_status) { + _relays[id].target_status = lock; + continue; + } + break; + } + case RELAY_LOCK_DISABLED: + default: + break; + } + // Only process if the change_time has arrived if (current_time < _relays[id].change_time) continue; @@ -203,7 +259,7 @@ void _relayProcess(bool mode) { _relaySaveTicker.once_ms(RELAY_SAVE_DELAY, relaySave, save_eeprom); #if WEB_SUPPORT - wsSend(_relayWebSocketUpdate); + wsPost(_relayWebSocketUpdate); #endif } @@ -352,10 +408,10 @@ bool relayStatus(unsigned char id, bool status) { bool relayStatus(unsigned char id) { - // Check relay ID + // Check that relay ID is valid if (id >= _relays.size()) return false; - // Get status from storage + // Get status directly from storage return _relays[id].current_status; } @@ -459,37 +515,40 @@ unsigned char relayCount() { return _relays.size(); } -unsigned char relayParsePayload(const char * payload) { - - // Payload could be "OFF", "ON", "TOGGLE" - // or its number equivalents: 0, 1 or 2 +RelayStatus relayParsePayload(const char * payload) { - if (payload[0] == '0') return 0; - if (payload[0] == '1') return 1; - if (payload[0] == '2') return 2; + // Don't parse empty strings + const auto len = strlen(payload); + if (!len) return RelayStatus::UNKNOWN; - // trim payload - char * p = ltrim((char *)payload); - - // to lower - unsigned int l = strlen(p); - if (l>6) l=6; - for (unsigned char i=0; i(stored_mask); // Walk the relays + unsigned char lock; bool status; for (unsigned char i=0; i(_relays[i].target_status); + status.add(_relays[i].target_status); + lock.add(_relays[i].lock); } } @@ -649,9 +719,7 @@ String _relayFriendlyName(unsigned char i) { return res; } -void _relayWebSocketSendRelays() { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); +void _relayWebSocketSendRelays(JsonObject& root) { JsonObject& relays = root.createNestedObject("relayConfig"); relays["size"] = relayCount(); @@ -686,69 +754,52 @@ void _relayWebSocketSendRelays() { on_disconnect.add(getSetting("relayOnDisc", i, 0).toInt()); #endif } - - wsSend(root); } -void _relayWebSocketOnStart(JsonObject& root) { - +void _relayWebSocketOnVisible(JsonObject& root) { if (relayCount() == 0) return; - // Per-relay configuration - _relayWebSocketSendRelays(); - - // Statuses - _relayWebSocketUpdate(root); - - // Options if (relayCount() > 1) { root["multirelayVisible"] = 1; root["relaySync"] = getSetting("relaySync", RELAY_SYNC); } root["relayVisible"] = 1; - } -void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { +void _relayWebSocketOnConnected(JsonObject& root) { - if (strcmp(action, "relay") != 0) return; - - if (data.containsKey("status")) { - - unsigned char value = relayParsePayload(data["status"]); + if (relayCount() == 0) return; - if (value == 3) { + // Per-relay configuration + _relayWebSocketSendRelays(root); - wsSend(_relayWebSocketUpdate); +} - } else if (value < 3) { +void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { - unsigned int relayID = 0; - if (data.containsKey("id")) { - String value = data["id"]; - relayID = value.toInt(); - } + if (strcmp(action, "relay") != 0) return; - // Action to perform - if (value == 0) { - relayStatus(relayID, false); - } else if (value == 1) { - relayStatus(relayID, true); - } else if (value == 2) { - relayToggle(relayID); - } + if (data.containsKey("status")) { + unsigned int relayID = 0; + if (data.containsKey("id") && data.is("id")) { + relayID = data["id"]; } + _relayHandlePayload(relayID, data["status"]); + } } void relaySetupWS() { - wsOnSendRegister(_relayWebSocketOnStart); - wsOnActionRegister(_relayWebSocketOnAction); - wsOnReceiveRegister(_relayWebSocketOnReceive); + wsRegister() + .onVisible(_relayWebSocketOnVisible) + .onConnected(_relayWebSocketOnConnected) + .onData(_relayWebSocketUpdate) + .onAction(_relayWebSocketOnAction) + .onKeyCheck(_relayWebSocketOnKeyCheck); } #endif // WEB_SUPPORT @@ -773,32 +824,22 @@ void relaySetupAPI() { }, [relayID](const char * payload) { - unsigned char value = relayParsePayload(payload); - - if (value == 0xFF) { + if (_relayHandlePayload(relayID, payload)) { DEBUG_MSG_P(PSTR("[RELAY] Wrong payload (%s)\n"), payload); return; } - if (value == 0) { - relayStatus(relayID, false); - } else if (value == 1) { - relayStatus(relayID, true); - } else if (value == 2) { - relayToggle(relayID); - } - } ); snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_PULSE, relayID); apiRegister(key, [relayID](char * buffer, size_t len) { - dtostrf((double) _relays[relayID].pulse_ms / 1000, 1-len, 3, buffer); + dtostrf((double) _relays[relayID].pulse_ms / 1000, 1, 3, buffer); }, [relayID](const char * payload) { - unsigned long pulse = 1000 * String(payload).toFloat(); + unsigned long pulse = 1000 * atof(payload); if (0 == pulse) return; if (RELAY_PULSE_NONE != _relays[relayID].pulse) { @@ -837,6 +878,31 @@ void relaySetupAPI() { #if MQTT_SUPPORT +const String& relayPayloadOn() { + return _relay_mqtt_payload_on; +} + +const String& relayPayloadOff() { + return _relay_mqtt_payload_off; +} + +const String& relayPayloadToggle() { + return _relay_mqtt_payload_toggle; +} + +const char* relayPayload(RelayStatus status) { + + if (status == RelayStatus::OFF) { + return _relay_mqtt_payload_off.c_str(); + } else if (status == RelayStatus::ON) { + return _relay_mqtt_payload_on.c_str(); + } else if (status == RelayStatus::TOGGLE) { + return _relay_mqtt_payload_toggle.c_str(); + } + + return ""; +} + void _relayMQTTGroup(unsigned char id) { String topic = getSetting("mqttGroup", id, ""); if (!topic.length()) return; @@ -844,9 +910,9 @@ void _relayMQTTGroup(unsigned char id) { unsigned char mode = getSetting("mqttGroupSync", id, RELAY_GROUP_SYNC_NORMAL).toInt(); if (mode == RELAY_GROUP_SYNC_RECEIVEONLY) return; - bool status = relayStatus(id); - if (mode == RELAY_GROUP_SYNC_INVERSE) status = !status; - mqttSendRaw(topic.c_str(), status ? RELAY_MQTT_ON : RELAY_MQTT_OFF); + auto status = _relayStatusTyped(id); + if (mode == RELAY_GROUP_SYNC_INVERSE) status = _relayStatusInvert(status); + mqttSendRaw(topic.c_str(), relayPayload(status)); } void relayMQTT(unsigned char id) { @@ -856,7 +922,7 @@ void relayMQTT(unsigned char id) { // Send state topic if (_relays[id].report) { _relays[id].report = false; - mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF); + mqttSend(MQTT_TOPIC_RELAY, id, relayPayload(_relayStatusTyped(id))); } // Check group topic @@ -876,19 +942,19 @@ void relayMQTT(unsigned char id) { void relayMQTT() { for (unsigned int id=0; id < _relays.size(); id++) { - mqttSend(MQTT_TOPIC_RELAY, id, _relays[id].current_status ? RELAY_MQTT_ON : RELAY_MQTT_OFF); + mqttSend(MQTT_TOPIC_RELAY, id, relayPayload(_relayStatusTyped(id))); } } -void relayStatusWrap(unsigned char id, unsigned char value, bool is_group_topic) { +void relayStatusWrap(unsigned char id, RelayStatus value, bool is_group_topic) { switch (value) { - case 0: + case RelayStatus::OFF: relayStatus(id, false, mqttForward(), !is_group_topic); break; - case 1: + case RelayStatus::ON: relayStatus(id, true, mqttForward(), !is_group_topic); break; - case 2: + case RelayStatus::TOGGLE: relayToggle(id, true, true); break; default: @@ -943,7 +1009,7 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo return; } - unsigned long pulse = 1000 * String(payload).toFloat(); + unsigned long pulse = 1000 * atof(payload); if (0 == pulse) return; if (RELAY_PULSE_NONE != _relays[id].pulse) { @@ -969,8 +1035,8 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo } // Get value - unsigned char value = relayParsePayload(payload); - if (value == 0xFF) return; + auto value = relayParsePayload(payload); + if (value == RelayStatus::UNKNOWN) return; relayStatusWrap(id, value, false); @@ -985,12 +1051,12 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo if ((t.length() > 0) && t.equals(topic)) { - unsigned char value = relayParsePayload(payload); - if (value == 0xFF) return; + auto value = relayParsePayload(payload); + if (value == RelayStatus::UNKNOWN) return; - if (value < 2) { + if ((value == RelayStatus::ON) || (value == RelayStatus::OFF)) { if (getSetting("mqttGroupSync", i, RELAY_GROUP_SYNC_NORMAL).toInt() == RELAY_GROUP_SYNC_INVERSE) { - value = 1 - value; + value = _relayStatusInvert(value); } } @@ -1014,10 +1080,10 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo int reaction = getSetting("relayOnDisc", i, 0).toInt(); if (1 == reaction) { // switch relay OFF DEBUG_MSG_P(PSTR("[RELAY] Reset relay (%d) due to MQTT disconnection\n"), i); - relayStatusWrap(i, false, false); + relayStatusWrap(i, RelayStatus::OFF, false); } else if(2 == reaction) { // switch relay ON DEBUG_MSG_P(PSTR("[RELAY] Set relay (%d) due to MQTT disconnection\n"), i); - relayStatusWrap(i, true, false); + relayStatusWrap(i, RelayStatus::ON, false); } } diff --git a/code/espurna/rfbridge.ino b/code/espurna/rfbridge.ino index 58ab0305..4d01cefa 100644 --- a/code/espurna/rfbridge.ino +++ b/code/espurna/rfbridge.ino @@ -69,10 +69,6 @@ bool _rfb_receive = false; bool _rfb_transmit = false; unsigned char _rfb_repeat = RF_SEND_TIMES; -#if WEB_SUPPORT - Ticker _rfb_sendcodes; -#endif - // ----------------------------------------------------------------------------- // PRIVATES // ----------------------------------------------------------------------------- @@ -89,11 +85,7 @@ static bool _rfbToChar(byte * in, char * out, int n = RF_MESSAGE_SIZE) { #if WEB_SUPPORT -void _rfbWebSocketSendCodeArray(unsigned char start, unsigned char size) { - - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - +void _rfbWebSocketSendCodeArray(JsonObject& root, unsigned char start, unsigned char size) { JsonObject& rfb = root.createNestedObject("rfb"); rfb["size"] = size; rfb["start"] = start; @@ -105,21 +97,13 @@ void _rfbWebSocketSendCodeArray(unsigned char start, unsigned char size) { on.add(rfbRetrieve(id, true)); off.add(rfbRetrieve(id, false)); } - - wsSend(root); - } -void _rfbWebSocketSendCode(unsigned char id) { - _rfbWebSocketSendCodeArray(id, 1); -} - -void _rfbWebSocketSendCodes() { - _rfbWebSocketSendCodeArray(0, relayCount()); +void _rfbWebSocketOnVisible(JsonObject& root) { + root["rfbVisible"] = 1; } -void _rfbWebSocketOnSend(JsonObject& root) { - root["rfbVisible"] = 1; +void _rfbWebSocketOnConnected(JsonObject& root) { root["rfbRepeat"] = getSetting("rfbRepeat", RF_SEND_TIMES).toInt(); root["rfbCount"] = relayCount(); #if RFB_DIRECT @@ -127,7 +111,6 @@ void _rfbWebSocketOnSend(JsonObject& root) { root["rfbRX"] = getSetting("rfbRX", RFB_RX_PIN).toInt(); root["rfbTX"] = getSetting("rfbTX", RFB_TX_PIN).toInt(); #endif - _rfb_sendcodes.once_ms(1000, _rfbWebSocketSendCodes); } void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { @@ -136,10 +119,14 @@ void _rfbWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& if (strcmp(action, "rfbsend") == 0) rfbStore(data["id"], data["status"], data["data"].as()); } -bool _rfbWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _rfbWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "rfb", 3) == 0); } +void _rfbWebSocketOnData(JsonObject& root) { + _rfbWebSocketSendCodeArray(root, 0, relayCount()); +} + #endif // WEB_SUPPORT /* @@ -267,9 +254,6 @@ void _rfbDecode() { if (action == RF_CODE_LEARN_KO) { _rfbAck(); DEBUG_MSG_P(PSTR("[RF] Learn timeout\n")); - #if WEB_SUPPORT - wsSend_P(PSTR("{\"action\": \"rfbTimeout\"}")); - #endif } if (action == RF_CODE_LEARN_OK || action == RF_CODE_RFIN) { @@ -288,7 +272,9 @@ void _rfbDecode() { // Websocket update #if WEB_SUPPORT - _rfbWebSocketSendCode(_learnId); + wsPost([](JsonObject& root) { + _rfbWebSocketSendCodeArray(root, _learnId, 1); + }); #endif } @@ -518,18 +504,6 @@ void _rfbReceive() { #endif // RFB_DIRECT -void _rfbLearn() { - - _rfbLearnImpl(); - - #if WEB_SUPPORT - char buffer[100]; - snprintf_P(buffer, sizeof(buffer), PSTR("{\"action\": \"rfbLearn\", \"data\":{\"id\": %d, \"status\": %d}}"), _learnId, _learnStatus ? 1 : 0); - wsSend(buffer); - #endif - -} - #if MQTT_SUPPORT void _rfbMqttCallback(unsigned int type, const char * topic, const char * payload) { @@ -564,7 +538,7 @@ void _rfbMqttCallback(unsigned int type, const char * topic, const char * payloa return; } _learnStatus = (char)payload[0] != '0'; - _rfbLearn(); + _rfbLearnImpl(); return; } @@ -615,7 +589,7 @@ void _rfbAPISetup() { tok = strtok(NULL, ","); if (NULL == tok) return; _learnStatus = (char) tok[0] != '0'; - _rfbLearn(); + _rfbLearnImpl(); } ); @@ -735,7 +709,7 @@ void rfbStatus(unsigned char id, bool status) { void rfbLearn(unsigned char id, bool status) { _learnId = id; _learnStatus = status; - _rfbLearn(); + _rfbLearnImpl(); } void rfbForget(unsigned char id, bool status) { @@ -746,9 +720,9 @@ void rfbForget(unsigned char id, bool status) { // Websocket update #if WEB_SUPPORT - char wsb[100]; - snprintf_P(wsb, sizeof(wsb), PSTR("{\"rfb\":[{\"id\": %d, \"status\": %d, \"data\": \"\"}]}"), id, status ? 1 : 0); - wsSend(wsb); + wsPost([id](JsonObject& root) { + _rfbWebSocketSendCodeArray(root, id, 1); + }); #endif } @@ -768,9 +742,12 @@ void rfbSetup() { #endif #if WEB_SUPPORT - wsOnSendRegister(_rfbWebSocketOnSend); - wsOnActionRegister(_rfbWebSocketOnAction); - wsOnReceiveRegister(_rfbWebSocketOnReceive); + wsRegister() + .onVisible(_rfbWebSocketOnVisible) + .onConnected(_rfbWebSocketOnConnected) + .onData(_rfbWebSocketOnData) + .onAction(_rfbWebSocketOnAction) + .onKeyCheck(_rfbWebSocketOnKeyCheck); #endif #if TERMINAL_SUPPORT diff --git a/code/espurna/rfm69.ino b/code/espurna/rfm69.ino index f24ca04c..73f05cc9 100644 --- a/code/espurna/rfm69.ino +++ b/code/espurna/rfm69.ino @@ -35,7 +35,7 @@ unsigned long _rfm69_packet_count; #if WEB_SUPPORT -void _rfm69WebSocketOnSend(JsonObject& root) { +void _rfm69WebSocketOnConnected(JsonObject& root) { root["rfm69Visible"] = 1; root["rfm69Topic"] = getSetting("rfm69Topic", RFM69_DEFAULT_TOPIC); @@ -53,7 +53,7 @@ void _rfm69WebSocketOnSend(JsonObject& root) { } -bool _rfm69WebSocketOnReceive(const char * key, JsonVariant& value) { +bool _rfm69WebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "rfm69", 5) == 0) return true; if (strncmp(key, "node", 4) == 0) return true; if (strncmp(key, "key", 3) == 0) return true; @@ -269,9 +269,10 @@ void rfm69Setup() { DEBUG_MSG_P(PSTR("[RFM69] Promiscuous mode %s\n"), RFM69_PROMISCUOUS ? "ON" : "OFF"); #if WEB_SUPPORT - wsOnSendRegister(_rfm69WebSocketOnSend); - wsOnReceiveRegister(_rfm69WebSocketOnReceive); - wsOnActionRegister(_rfm69WebSocketOnAction); + wsRegister() + .onConnected(_rfm69WebSocketOnConnected) + .onAction(_rfm69WebSocketOnAction) + .onKeyCheck(_rfm69WebSocketOnKeyCheck); #endif // Main callbacks diff --git a/code/espurna/rtcmem.ino b/code/espurna/rtcmem.ino index df86b74a..634693bf 100644 --- a/code/espurna/rtcmem.ino +++ b/code/espurna/rtcmem.ino @@ -1,7 +1,22 @@ +/* + +RTMEM MODULE + +*/ + bool _rtcmem_status = false; +void _rtcmemErase() { + auto ptr = reinterpret_cast(RTCMEM_ADDR); + const auto end = ptr + RTCMEM_BLOCKS; + DEBUG_MSG_P(PSTR("[RTCMEM] Erasing start=%p end=%p\n"), ptr, end); + do { + *ptr = 0; + } while (++ptr != end); +} + void _rtcmemInit() { - memset((uint32_t*)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS); + _rtcmemErase(); Rtcmem->magic = RTCMEM_MAGIC; } @@ -31,17 +46,38 @@ void _rtcmemInitCommands() { _rtcmemInit(); }); - terminalRegisterCommand(F("RTCMEM.TEST"), [](Embedis* e) { - }); - terminalRegisterCommand(F("RTCMEM.DUMP"), [](Embedis* e) { - DEBUG_MSG_P(PSTR("[RTCMEM] status:%u blocks:%u addr:0x%p\n"), - _rtcmemStatus(), RtcmemSize, Rtcmem); - for (uint8_t block=0; block(RTCMEM_ADDR)[block]); - } + DEBUG_MSG_P(PSTR("[RTCMEM] boot_status=%u status=%u blocks_used=%u\n"), + _rtcmem_status, _rtcmemStatus(), RtcmemSize); + + String line; + line.reserve(96); + char buffer[16] = {0}; + + auto addr = reinterpret_cast(RTCMEM_ADDR); + + uint8_t block = 1; + uint8_t offset = 0; + uint8_t start = 0; + + do { + + offset = block - 1; + + snprintf(buffer, sizeof(buffer), "%08x ", *(addr + offset)); + line += buffer; + + if ((block % 8) == 0) { + DEBUG_MSG_P(PSTR("%02u %p: %s\n"), start, addr+start, line.c_str()); + start = block; + line = ""; + } + + ++block; + + } while (block<(RTCMEM_BLOCKS+1)); + }); } diff --git a/code/espurna/scheduler.ino b/code/espurna/scheduler.ino index 7e2debfc..ac62069c 100644 --- a/code/espurna/scheduler.ino +++ b/code/espurna/scheduler.ino @@ -15,15 +15,19 @@ Adapted by Xose Pérez #if WEB_SUPPORT -bool _schWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _schWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "sch", 3) == 0); } -void _schWebSocketOnSend(JsonObject &root){ +void _schWebSocketOnVisible(JsonObject& root) { + if (!relayCount()) return; + root["schVisible"] = 1; +} + +void _schWebSocketOnConnected(JsonObject &root){ if (!relayCount()) return; - root["schVisible"] = 1; root["maxSchedules"] = SCHEDULER_MAX_SCHEDULES; JsonObject &schedules = root.createNestedObject("schedules"); @@ -229,8 +233,10 @@ void schSetup() { // Update websocket clients #if WEB_SUPPORT - wsOnSendRegister(_schWebSocketOnSend); - wsOnReceiveRegister(_schWebSocketOnReceive); + wsRegister() + .onVisible(_schWebSocketOnVisible) + .onConnected(_schWebSocketOnConnected) + .onKeyCheck(_schWebSocketOnKeyCheck); #endif // Main callbacks diff --git a/code/espurna/sensor.ino b/code/espurna/sensor.ino index b8a85481..013f43c8 100644 --- a/code/espurna/sensor.ino +++ b/code/espurna/sensor.ino @@ -17,7 +17,7 @@ Copyright (C) 2016-2019 by Xose Pérez #include -typedef struct { +struct sensor_magnitude_t { BaseSensor * sensor; // Sensor object BaseFilter * filter; // Filter object unsigned char local; // Local index in its provider @@ -28,7 +28,7 @@ typedef struct { double reported; // Last reported value double min_change; // Minimum value change to report double max_change; // Maximum value change to report -} sensor_magnitude_t; +}; std::vector _sensors; std::vector _magnitudes; @@ -111,32 +111,46 @@ double _magnitudeProcess(unsigned char type, unsigned char decimals, double valu #if WEB_SUPPORT +//void _sensorWebSocketMagnitudes(JsonObject& root, const String& ws_name, const String& conf_name) { template void _sensorWebSocketMagnitudes(JsonObject& root, T prefix) { // ws produces flat list Magnitudes - String ws_name = String(prefix); - ws_name.concat("Magnitudes"); + const String ws_name = String(prefix) + "Magnitudes"; // config uses Magnitude (cut 's') - String conf_name = ws_name.substring(0, ws_name.length() - 1); + const String conf_name = ws_name.substring(0, ws_name.length() - 1); JsonObject& list = root.createNestedObject(ws_name); list["size"] = magnitudeCount(); - JsonArray& name = list.createNestedArray("name"); + //JsonArray& name = list.createNestedArray("name"); JsonArray& type = list.createNestedArray("type"); JsonArray& index = list.createNestedArray("index"); JsonArray& idx = list.createNestedArray("idx"); for (unsigned char i=0; i void _sensorWebSocketMagnitudes(JsonObject& root, T prefix) { + + // ws produces flat list Magnitudes + const String ws_name = String(prefix) + "Magnitudes"; + + // config uses Magnitude (cut 's') + const String conf_name = ws_name.substring(0, ws_name.length() - 1); + + _sensorWebSocketMagnitudes(root, ws_name, conf_name); + +} +*/ + +bool _sensorWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "pwr", 3) == 0) return true; if (strncmp(key, "sns", 3) == 0) return true; if (strncmp(key, "tmp", 3) == 0) return true; @@ -146,21 +160,28 @@ bool _sensorWebSocketOnReceive(const char * key, JsonVariant& value) { return false; } -void _sensorWebSocketSendData(JsonObject& root) { +void _sensorWebSocketOnVisible(JsonObject& root) { - char buffer[10]; - bool hasTemperature = false; - bool hasHumidity = false; - bool hasMICS = false; + root["snsVisible"] = 1; - JsonObject& magnitudes = root.createNestedObject("magnitudes"); + for (auto& magnitude : _magnitudes) { + if (magnitude.type == MAGNITUDE_TEMPERATURE) root["temperatureVisible"] = 1; + if (magnitude.type == MAGNITUDE_HUMIDITY) root["humidityVisible"] = 1; + #if MICS2710_SUPPORT || MICS5525_SUPPORT + if (magnitude.type == MAGNITUDE_CO || magnitude.type == MAGNITUDE_NO2) root["micsVisible"] = 1; + #endif + } + +} + +void _sensorWebSocketMagnitudesConfig(JsonObject& root) { + + JsonObject& magnitudes = root.createNestedObject("magnitudesConfig"); uint8_t size = 0; JsonArray& index = magnitudes.createNestedArray("index"); JsonArray& type = magnitudes.createNestedArray("type"); - JsonArray& value = magnitudes.createNestedArray("value"); JsonArray& units = magnitudes.createNestedArray("units"); - JsonArray& error = magnitudes.createNestedArray("error"); JsonArray& description = magnitudes.createNestedArray("description"); for (unsigned char i=0; i(magnitude.global); type.add(magnitude.type); - value.add(buffer); units.add(magnitudeUnits(magnitude.type)); - error.add(magnitude.sensor->error()); if (magnitude.type == MAGNITUDE_ENERGY) { if (_sensor_energy_reset_ts.length() == 0) _sensorResetTS(); @@ -185,22 +201,39 @@ void _sensorWebSocketSendData(JsonObject& root) { description.add(magnitude.sensor->slot(magnitude.local)); } - if (magnitude.type == MAGNITUDE_TEMPERATURE) hasTemperature = true; - if (magnitude.type == MAGNITUDE_HUMIDITY) hasHumidity = true; - #if MICS2710_SUPPORT || MICS5525_SUPPORT - if (magnitude.type == MAGNITUDE_CO || magnitude.type == MAGNITUDE_NO2) hasMICS = true; - #endif } magnitudes["size"] = size; - if (hasTemperature) root["temperatureVisible"] = 1; - if (hasHumidity) root["humidityVisible"] = 1; - if (hasMICS) root["micsVisible"] = 1; +} + +void _sensorWebSocketSendData(JsonObject& root) { + + char buffer[64]; + + JsonObject& magnitudes = root.createNestedObject("magnitudes"); + uint8_t size = 0; + + JsonArray& value = magnitudes.createNestedArray("value"); + JsonArray& error = magnitudes.createNestedArray("error"); + + for (unsigned char i=0; ierror()); + } + + magnitudes["size"] = size; } -void _sensorWebSocketStart(JsonObject& root) { +void _sensorWebSocketOnConnected(JsonObject& root) { for (unsigned char i=0; i<_sensors.size(); i++) { @@ -257,7 +290,6 @@ void _sensorWebSocketStart(JsonObject& root) { } if (magnitudeCount()) { - root["snsVisible"] = 1; //root["apiRealTime"] = _sensor_realtime; root["pwrUnits"] = _sensor_power_units; root["eneUnits"] = _sensor_energy_units; @@ -267,6 +299,7 @@ void _sensorWebSocketStart(JsonObject& root) { root["snsRead"] = _sensor_read_interval / 1000; root["snsReport"] = _sensor_report_every; root["snsSave"] = _sensor_save_every; + _sensorWebSocketMagnitudesConfig(root); } /* @@ -304,7 +337,7 @@ void _sensorAPISetup() { apiRegister(topic.c_str(), [magnitude_id](char * buffer, size_t len) { sensor_magnitude_t magnitude = _magnitudes[magnitude_id]; double value = _sensor_realtime ? magnitude.last : magnitude.reported; - dtostrf(value, 1-len, magnitude.decimals, buffer); + dtostrf(value, 1, magnitude.decimals, buffer); }); } @@ -360,7 +393,7 @@ void _sensorInitCommands() { DEBUG_MSG_P(PSTR("[SENSOR] PZEM004T\n")); for(unsigned char dev = init; dev < limit; dev++) { float offset = pzem004t_sensor->resetEnergy(dev); - setSetting("pzemEneTotal", dev, offset); + _sensorEnergyTotal(dev, offset); DEBUG_MSG_P(PSTR("Device %d/%s - Offset: %s\n"), dev, pzem004t_sensor->getAddress(dev).c_str(), String(offset).c_str()); } terminalOK(); @@ -429,33 +462,38 @@ void _sensorResetTS() { #endif } -double _sensorEnergyTotal() { +double _sensorEnergyTotal(unsigned int index) { double value = 0; - if (rtcmemStatus()) { - value = Rtcmem->energy; + if (rtcmemStatus() && (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy)))) { + value = Rtcmem->energy[index]; } else { - value = (_sensor_save_every > 0) ? getSetting("eneTotal", 0).toInt() : 0; + value = (_sensor_save_every > 0) ? getSetting("eneTotal", index, 0).toInt() : 0; } return value; } +double _sensorEnergyTotal() { + return _sensorEnergyTotal(0); +} -void _sensorEnergyTotal(double value) { +void _sensorEnergyTotal(unsigned int index, double value) { static unsigned long save_count = 0; // Save to EEPROM every '_sensor_save_every' readings if (_sensor_save_every > 0) { save_count = (save_count + 1) % _sensor_save_every; if (0 == save_count) { - setSetting("eneTotal", value); + setSetting("eneTotal", index, value); saveSettings(); } } // Always save to RTCMEM - Rtcmem->energy = value; + if (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy))) { + Rtcmem->energy[index] = value; + } } // ----------------------------------------------------------------------------- @@ -574,11 +612,85 @@ void _sensorLoad() { #if DIGITAL_SUPPORT { - DigitalSensor * sensor = new DigitalSensor(); - sensor->setGPIO(DIGITAL_PIN); - sensor->setMode(DIGITAL_PIN_MODE); - sensor->setDefault(DIGITAL_DEFAULT_STATE); - _sensors.push_back(sensor); + #if (DIGITAL1_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL1_PIN); + sensor->setMode(DIGITAL1_PIN_MODE); + sensor->setDefault(DIGITAL1_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL2_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL2_PIN); + sensor->setMode(DIGITAL2_PIN_MODE); + sensor->setDefault(DIGITAL2_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL3_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL3_PIN); + sensor->setMode(DIGITAL3_PIN_MODE); + sensor->setDefault(DIGITAL3_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL4_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL4_PIN); + sensor->setMode(DIGITAL4_PIN_MODE); + sensor->setDefault(DIGITAL4_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL5_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL5_PIN); + sensor->setMode(DIGITAL5_PIN_MODE); + sensor->setDefault(DIGITAL5_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL6_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL6_PIN); + sensor->setMode(DIGITAL6_PIN_MODE); + sensor->setDefault(DIGITAL6_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL7_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL7_PIN); + sensor->setMode(DIGITAL7_PIN_MODE); + sensor->setDefault(DIGITAL7_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif + + #if (DIGITAL8_PIN != GPIO_NONE) + { + DigitalSensor * sensor = new DigitalSensor(); + sensor->setGPIO(DIGITAL8_PIN); + sensor->setMode(DIGITAL8_PIN_MODE); + sensor->setDefault(DIGITAL8_DEFAULT_STATE); + _sensors.push_back(sensor); + } + #endif } #endif @@ -631,13 +743,101 @@ void _sensorLoad() { #if EVENTS_SUPPORT { - EventSensor * sensor = new EventSensor(); - sensor->setGPIO(EVENTS_PIN); - sensor->setTrigger(EVENTS_TRIGGER); - sensor->setPinMode(EVENTS_PIN_MODE); - sensor->setDebounceTime(EVENTS_DEBOUNCE); - sensor->setInterruptMode(EVENTS_INTERRUPT_MODE); - _sensors.push_back(sensor); + #if (EVENTS1_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS1_PIN); + sensor->setTrigger(EVENTS1_TRIGGER); + sensor->setPinMode(EVENTS1_PIN_MODE); + sensor->setDebounceTime(EVENTS1_DEBOUNCE); + sensor->setInterruptMode(EVENTS1_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS2_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS2_PIN); + sensor->setTrigger(EVENTS2_TRIGGER); + sensor->setPinMode(EVENTS2_PIN_MODE); + sensor->setDebounceTime(EVENTS2_DEBOUNCE); + sensor->setInterruptMode(EVENTS2_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS3_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS3_PIN); + sensor->setTrigger(EVENTS3_TRIGGER); + sensor->setPinMode(EVENTS3_PIN_MODE); + sensor->setDebounceTime(EVENTS3_DEBOUNCE); + sensor->setInterruptMode(EVENTS3_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS4_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS4_PIN); + sensor->setTrigger(EVENTS4_TRIGGER); + sensor->setPinMode(EVENTS4_PIN_MODE); + sensor->setDebounceTime(EVENTS4_DEBOUNCE); + sensor->setInterruptMode(EVENTS4_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS5_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS5_PIN); + sensor->setTrigger(EVENTS5_TRIGGER); + sensor->setPinMode(EVENTS5_PIN_MODE); + sensor->setDebounceTime(EVENTS5_DEBOUNCE); + sensor->setInterruptMode(EVENTS5_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS6_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS6_PIN); + sensor->setTrigger(EVENTS6_TRIGGER); + sensor->setPinMode(EVENTS6_PIN_MODE); + sensor->setDebounceTime(EVENTS6_DEBOUNCE); + sensor->setInterruptMode(EVENTS6_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS7_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS7_PIN); + sensor->setTrigger(EVENTS7_TRIGGER); + sensor->setPinMode(EVENTS7_PIN_MODE); + sensor->setDebounceTime(EVENTS7_DEBOUNCE); + sensor->setInterruptMode(EVENTS7_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif + + #if (EVENTS8_PIN != GPIO_NONE) + { + EventSensor * sensor = new EventSensor(); + sensor->setGPIO(EVENTS8_PIN); + sensor->setTrigger(EVENTS8_TRIGGER); + sensor->setPinMode(EVENTS8_PIN_MODE); + sensor->setDebounceTime(EVENTS8_DEBOUNCE); + sensor->setInterruptMode(EVENTS8_INTERRUPT_MODE); + _sensors.push_back(sensor); + } + #endif } #endif @@ -756,9 +956,11 @@ void _sensorLoad() { #if PULSEMETER_SUPPORT { + PulseMeterSensor * sensor = new PulseMeterSensor(); sensor->setGPIO(PULSEMETER_PIN); sensor->setEnergyRatio(PULSEMETER_ENERGY_RATIO); + sensor->setInterruptMode(PULSEMETER_INTERRUPT_ON); sensor->setDebounceTime(PULSEMETER_DEBOUNCE); _sensors.push_back(sensor); } @@ -785,7 +987,7 @@ void _sensorLoad() { // Read saved energy offset unsigned char dev_count = sensor->getAddressesCount(); for(unsigned char dev = 0; dev < dev_count; dev++) { - float value = getSetting("pzemEneTotal", dev, 0).toFloat(); + float value = _sensorEnergyTotal(dev); if (value > 0) sensor->resetEnergy(dev, value); } _sensors.push_back(sensor); @@ -880,6 +1082,14 @@ void _sensorLoad() { _sensors.push_back(sensor); } #endif + + #if ADE7953_SUPPORT + { + ADE7953Sensor * sensor = new ADE7953Sensor(); + sensor->setAddress(ADE7953_ADDRESS); + _sensors.push_back(sensor); + } + #endif } void _sensorCallback(unsigned char i, unsigned char type, double value) { @@ -1019,6 +1229,19 @@ void _sensorInit() { #endif // HLW8012_SUPPORT + #if ADE7953_SUPPORT + + if (_sensors[i]->getID() == SENSOR_ADE7953_ID) { + ADE7953Sensor * sensor = (ADE7953Sensor *) _sensors[i]; + unsigned int dev_count = sensor->getTotalDevices(); + for(unsigned char dev = 0; dev < dev_count; dev++) { + double value = _sensorEnergyTotal(dev); + if (value > 0) sensor->resetEnergy(dev, value); + } + } + + #endif // ADE7953_SUPPORT + #if CSE7766_SUPPORT if (_sensors[i]->getID() == SENSOR_CSE7766_ID) { @@ -1046,7 +1269,7 @@ void _sensorInit() { #if PULSEMETER_SUPPORT if (_sensors[i]->getID() == SENSOR_PULSEMETER_ID) { PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i]; - sensor->setEnergyRatio(getSetting("pwrRatioE", PULSEMETER_ENERGY_RATIO).toInt()); + sensor->setEnergyRatio(getSetting("pwrRatioE", sensor->getEnergyRatio()).toInt()); } #endif // PULSEMETER_SUPPORT @@ -1115,7 +1338,7 @@ void _sensorConfigure() { if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } @@ -1130,7 +1353,7 @@ void _sensorConfigure() { EmonADC121Sensor * sensor = (EmonADC121Sensor *) _sensors[i]; if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } } @@ -1141,7 +1364,7 @@ void _sensorConfigure() { EmonADS1X15Sensor * sensor = (EmonADS1X15Sensor *) _sensors[i]; if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } } @@ -1172,7 +1395,7 @@ void _sensorConfigure() { if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } @@ -1211,7 +1434,7 @@ void _sensorConfigure() { if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } @@ -1231,11 +1454,11 @@ void _sensorConfigure() { PulseMeterSensor * sensor = (PulseMeterSensor *) _sensors[i]; if (getSetting("pwrResetE", 0).toInt() == 1) { sensor->resetEnergy(); - delSetting("eneTotal"); + delSetting("eneTotal", 0); _sensorResetTS(); } - sensor->setEnergyRatio(getSetting("pwrRatioE", PULSEMETER_ENERGY_RATIO).toInt()); + sensor->setEnergyRatio(getSetting("pwrRatioE", sensor->getEnergyRatio()).toInt()); } #endif // PULSEMETER_SUPPORT @@ -1247,7 +1470,7 @@ void _sensorConfigure() { unsigned char dev_count = sensor->getAddressesCount(); for(unsigned char dev = 0; dev < dev_count; dev++) { sensor->resetEnergy(dev, 0); - delSetting("pzemEneTotal", dev); + delSetting("eneTotal", dev); } _sensorResetTS(); } @@ -1255,19 +1478,37 @@ void _sensorConfigure() { #endif // PZEM004T_SUPPORT - } + #if ADE7953_SUPPORT + + if (_sensors[i]->getID() == SENSOR_ADE7953_ID) { + ADE7953Sensor * sensor = (ADE7953Sensor *) _sensors[i]; + if (getSetting("pwrResetE", 0).toInt() == 1) { + unsigned char dev_count = sensor->getTotalDevices(); + for(unsigned char dev = 0; dev < dev_count; dev++) { + sensor->resetEnergy(dev); + delSetting("eneTotal", dev); + } + _sensorResetTS(); + } + } + + #endif // ADE7953_SUPPORT - // Update filter sizes - for (unsigned char i=0; i<_magnitudes.size(); i++) { - _magnitudes[i].filter->resize(_sensor_report_every); } - // General processing - if (0 == _sensor_save_every) { - delSetting("eneTotal"); + // Update filter sizes and reset energy if needed + { + const bool reset_saved_energy = 0 == _sensor_save_every; + for (unsigned char i=0; i<_magnitudes.size(); i++) { + _magnitudes[i].filter->resize(_sensor_report_every); + if ((_magnitudes[i].type == MAGNITUDE_ENERGY) && reset_saved_energy) { + delSetting("eneTotal", _magnitudes[i].global); + } + } } - // Save settings + // Remove calibration values + // TODO: do not use settings for one-shot calibration delSetting("snsResetCalibration"); delSetting("pwrExpectedP"); delSetting("pwrExpectedC"); @@ -1283,8 +1524,11 @@ void _sensorReport(unsigned char index, double value) { sensor_magnitude_t magnitude = _magnitudes[index]; unsigned char decimals = magnitude.decimals; - char buffer[10]; - dtostrf(value, 1-sizeof(buffer), decimals, buffer); + // XXX: ensure that the received 'value' will fit here + // dtostrf 2nd arg only controls leading zeroes and the + // 3rd is only for the part after the dot + char buffer[64]; + dtostrf(value, 1, decimals, buffer); #if BROKER_SUPPORT #if not BROKER_REAL_TIME @@ -1424,11 +1668,12 @@ String magnitudeUnits(unsigned char type) { void sensorSetup() { // Backwards compatibility + moveSetting("eneTotal", "eneTotal0"); moveSetting("powerUnits", "pwrUnits"); moveSetting("energyUnits", "eneUnits"); // Update PZEM004T energy total across multiple devices - moveSettings("pzEneTotal", "pzemEneTotal"); + moveSettings("pzEneTotal", "eneTotal"); // Load sensors _sensorLoad(); @@ -1439,9 +1684,11 @@ void sensorSetup() { // Websockets #if WEB_SUPPORT - wsOnSendRegister(_sensorWebSocketStart); - wsOnReceiveRegister(_sensorWebSocketOnReceive); - wsOnSendRegister(_sensorWebSocketSendData); + wsRegister() + .onVisible(_sensorWebSocketOnVisible) + .onConnected(_sensorWebSocketOnConnected) + .onData(_sensorWebSocketSendData) + .onKeyCheck(_sensorWebSocketOnKeyCheck); #endif // API @@ -1558,7 +1805,7 @@ void sensorLoop() { #if SENSOR_DEBUG { char buffer[64]; - dtostrf(value_show, 1-sizeof(buffer), magnitude.decimals, buffer); + dtostrf(value_show, 1, magnitude.decimals, buffer); DEBUG_MSG_P(PSTR("[SENSOR] %s - %s: %s%s\n"), magnitude.sensor->slot(magnitude.local).c_str(), magnitudeTopic(magnitude.type).c_str(), @@ -1591,10 +1838,9 @@ void sensorLoop() { _sensorReport(i, value_filtered); } // if (fabs(value_filtered - magnitude.reported) >= magnitude.min_change) - // Persist total energy value if (MAGNITUDE_ENERGY == magnitude.type) { - _sensorEnergyTotal(value_raw); + _sensorEnergyTotal(magnitude.global, value_raw); } } // if (report_count == 0) @@ -1606,7 +1852,7 @@ void sensorLoop() { _sensorPost(); #if WEB_SUPPORT - wsSend(_sensorWebSocketSendData); + wsPost(_sensorWebSocketSendData); #endif #if THINGSPEAK_SUPPORT diff --git a/code/espurna/sensors/ADE7953Sensor.h b/code/espurna/sensors/ADE7953Sensor.h new file mode 100644 index 00000000..5b920880 --- /dev/null +++ b/code/espurna/sensors/ADE7953Sensor.h @@ -0,0 +1,249 @@ +// ----------------------------------------------------------------------------- +// ADE7853 Sensor over I2C +// Copyright (C) 2017-2019 by Xose Pérez +// Implemented by Antonio López +// ----------------------------------------------------------------------------- + +#if SENSOR_SUPPORT && ADE7953_SUPPORT + +#pragma once + +#undef I2C_SUPPORT +#define I2C_SUPPORT 1 // Explicitly request I2C support. + +#include "Arduino.h" +#include "I2CSensor.h" +#include + +// ----------------------------------------------------------------------------- +// ADE7953 - Energy (Shelly 2.5) +// +// Based on datasheet from https://www.analog.com/en/products/ade7953.html +// Based on Tasmota code https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/xnrg_07_ade7953.ino +// +// I2C Address: 0x38 +// ----------------------------------------------------------------------------- + +#define ADE7953_PREF 1540 +#define ADE7953_UREF 26000 +#define ADE7953_IREF 10000 + +#define ADE7953_ALL_RELAYS 0 +#define ADE7953_RELAY_1 1 +#define ADE7953_RELAY_2 2 + +#define ADE7953_VOLTAGE 1 +#define ADE7953_TOTAL_DEVICES 3 + +class ADE7953Sensor : public I2CSensor { + + protected: + + struct reading_t { + float current = 0.0; + float power = 0.0; + float energy = 0.0; + }; + + public: + // --------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------- + ADE7953Sensor(): I2CSensor() { + _sensor_id = SENSOR_ADE7953_ID; + _readings.resize(ADE7953_TOTAL_DEVICES); + _energy_offsets.resize(ADE7953_TOTAL_DEVICES); + _count = _readings.size() * ADE7953_TOTAL_DEVICES + ADE7953_VOLTAGE; //10 + } + + // --------------------------------------------------------------------- + // Sensors API + // --------------------------------------------------------------------- + + // Initialization method, must be idempotent + void begin() { + if (!_dirty) return; + _init(); + _dirty = !_ready; + } + + // Descriptive name of the sensor + String description() { + char buffer[25]; + snprintf(buffer, sizeof(buffer), "ADE7953 @ I2C (0x%02X)", _address); + return String(buffer); + } + + // Descriptive name of the slot # index + String slot(unsigned char index) { + return description(); + }; + + // Type for slot # index + unsigned char type(unsigned char index) { + if (index == 0) return MAGNITUDE_VOLTAGE; + index = index % ADE7953_TOTAL_DEVICES; + if (index == 0) return MAGNITUDE_ENERGY; + if (index == 1) return MAGNITUDE_CURRENT; + if (index == 2) return MAGNITUDE_POWER_ACTIVE; + return MAGNITUDE_NONE; + } + + // Pre-read hook (usually to populate registers with up-to-date data) + void pre() { + uint32_t active_power1 = 0; + uint32_t active_power2 = 0; + uint32_t current_rms = 0; + uint32_t current_rms1 = 0; + uint32_t current_rms2 = 0; + uint32_t voltage_rms = 0; + + voltage_rms = read(_address, 0x31C); // Both relays + current_rms1 = read(_address, 0x31B); // Relay 1 + if (current_rms1 < 2000) { // No load threshold (20mA) + current_rms1 = 0; + active_power1 = 0; + } else { + active_power1 = (int32_t)read(_address, 0x313) * -1; // Relay 1 + active_power1 = (active_power1 > 0) ? active_power1 : 0; + } + current_rms2 = read(_address, 0x31A); // Relay 2 + if (current_rms2 < 2000) { // No load threshold (20mA) + current_rms2 = 0; + active_power2 = 0; + } else { + active_power2 = (int32_t)read(_address, 0x312); // Relay 2 + active_power2 = (active_power2 > 0) ? active_power2 : 0; + } + _voltage = (float) voltage_rms / ADE7953_UREF; + + storeReading( + ADE7953_ALL_RELAYS, + (float)(current_rms1 + current_rms2) / (ADE7953_IREF * 10), + (float)(active_power1 + active_power2) / (ADE7953_PREF / 10) + ); + storeReading( + ADE7953_RELAY_1, + (float) current_rms1 / (ADE7953_IREF * 10), + (float) active_power1 / (ADE7953_PREF / 10) + ); + storeReading( + ADE7953_RELAY_2, + (float)current_rms2 / (ADE7953_IREF * 10), + (float)active_power2 / (ADE7953_PREF / 10) + ); + } + + inline void storeReading(unsigned int relay, float current, float power) { + auto& reading_ref = _readings.at(relay); + reading_ref.current = current; + reading_ref.power = power; + static unsigned long last = 0; + if (last > 0) { + reading_ref.energy += (power * (millis() - last) / 1000); + } + last = millis(); + } + + // Current value for slot # index + double value(unsigned char index) { + if (index == 0) return _voltage; + int relay = (index - 1) / ADE7953_TOTAL_DEVICES; + index = index % ADE7953_TOTAL_DEVICES; + if (index == 0) return _energy_offsets[relay] + _readings[relay].energy; + if (index == 1) return _readings[relay].current; + if (index == 2) return _readings[relay].power; + return 0; + } + + unsigned int getTotalDevices() { + return ADE7953_TOTAL_DEVICES; + } + + void resetEnergy(int relay, double value = 0) { + _energy_offsets[relay] = value; + } + + protected: + void _init() { + nice_delay(100); // Need 100mS to init ADE7953 + write(_address, 0x102, 0x0004); // Locking the communication interface (Clear bit COMM_LOCK), Enable HPF + write(_address, 0x0FE, 0x00AD); // Unlock register 0x120 + write(_address, 0x120, 0x0030); // Configure optimum setting + + _ready = true; + } + + #if 0 + static int reg_size(uint16_t reg) { + int size = 0; + switch ((reg >> 8) & 0x0F) { + case 0x03: + size++; + case 0x02: + size++; + case 0x01: + size++; + case 0x00: + case 0x07: + case 0x08: + size++; + } + return size; + } + #else + // Optimized version of the function above, -80 bytes of code + // Use the known property of register addresses to calculate their size + static const int reg_size(const uint16_t reg) { + + const uint8_t mask = ((reg >> 8) & 0b1111); + + if (!mask || (mask & 0b1100)) { + return 1; + } else if (mask & 0b0011) { + return mask + 1; + } + + return 0; + + } + #endif + + void write(unsigned char address, uint16_t reg, uint32_t val) { + int size = reg_size(reg); + if (size) { + Wire.beginTransmission(address); + Wire.write((reg >> 8) & 0xFF); + Wire.write(reg & 0xFF); + while (size--) { + Wire.write((val >> (8 * size)) & 0xFF); // Write data, MSB first + } + Wire.endTransmission(); + delayMicroseconds(5); // Bus-free time minimum 4.7us + } + } + + static uint32_t read(int address, uint16_t reg) { + uint32_t response = 0; + const int size = reg_size(reg); + if (size) { + Wire.beginTransmission(address); + Wire.write((reg >> 8) & 0xFF); + Wire.write(reg & 0xFF); + Wire.endTransmission(0); + Wire.requestFrom(address, size); + if (size <= Wire.available()) { + for (int i = 0; i < size; i++) { + response = response << 8 | Wire.read(); // receive DATA (MSB first) + } + } + } + return response; + } + + std::vector _readings; + float _voltage = 0; + std::vector _energy_offsets; +}; + +#endif // SENSOR_SUPPORT && ADE7953_SUPPORT diff --git a/code/espurna/sensors/AnalogSensor.h b/code/espurna/sensors/AnalogSensor.h index e58363d9..c8f12be8 100644 --- a/code/espurna/sensors/AnalogSensor.h +++ b/code/espurna/sensors/AnalogSensor.h @@ -3,7 +3,7 @@ // Copyright (C) 2017-2019 by Xose Pérez // ----------------------------------------------------------------------------- -#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT || LDR_SENSOR) +#if SENSOR_SUPPORT && (ANALOG_SUPPORT || NTC_SUPPORT || LDR_SUPPORT) #pragma once @@ -68,8 +68,7 @@ class AnalogSensor : public BaseSensor { // --------------------------------------------------------------------- // Initialization method, must be idempotent - void begin() { - pinMode(0, INPUT); + void begin() { _ready = true; } diff --git a/code/espurna/sensors/EventSensor.h b/code/espurna/sensors/EventSensor.h index 4910e9b8..d12671a4 100644 --- a/code/espurna/sensors/EventSensor.h +++ b/code/espurna/sensors/EventSensor.h @@ -48,8 +48,8 @@ class EventSensor : public BaseSensor { _interrupt_mode = interrupt_mode; } - void setDebounceTime(unsigned long debounce) { - _debounce = debounce; + void setDebounceTime(unsigned long ms) { + _debounce = microsecondsToClockCycles(ms * 1000); } // --------------------------------------------------------------------- @@ -87,6 +87,16 @@ class EventSensor : public BaseSensor { _ready = true; } + void tick() { + if (!_trigger || !_callback) return; + if (!_trigger_flag) return; + + noInterrupts(); + _callback(MAGNITUDE_EVENT, _trigger_value); + _trigger_flag = false; + interrupts(); + } + // Descriptive name of the sensor String description() { char buffer[20]; @@ -114,33 +124,42 @@ class EventSensor : public BaseSensor { // Current value for slot # index double value(unsigned char index) { if (index == 0) { - double value = _events; - _events = 0; + double value = _counter; + _counter = 0; return value; }; + if (index == 1) { + return _value; + } return 0; } - // Handle interrupt calls + // Handle interrupt calls from isr[GPIO] functions void ICACHE_RAM_ATTR handleInterrupt(unsigned char gpio) { UNUSED(gpio); - static unsigned long last = 0; // clock count in 32bit value, overflowing: // ~53s when F_CPU is 80MHz // ~26s when F_CPU is 160MHz // see: cores/esp8266/Arduino.h definitions - unsigned long ms = clockCyclesToMicroseconds(ESP.getCycleCount()) / 1000u; - - if (ms - last > _debounce) { - - last = ms; - _events = _events + 1; - + // + // Note: + // To convert to / from normal time values, use: + // - microsecondsToClockCycles(microseconds) + // - clockCyclesToMicroseconds(cycles) + // Since the division operation on this chip is pretty slow, + // avoid doing the conversion here + unsigned long cycles = ESP.getCycleCount(); + + if (cycles - _last > _debounce) { + _last = cycles; + _counter += 1; + + // we are handling callbacks in tick() if (_trigger) { - if (_callback) _callback(MAGNITUDE_EVENT, digitalRead(gpio)); + _trigger_value = digitalRead(gpio); + _trigger_flag = true; } - } } @@ -168,10 +187,16 @@ class EventSensor : public BaseSensor { // Protected // --------------------------------------------------------------------- - volatile unsigned long _events = 0; - unsigned long _debounce = EVENTS_DEBOUNCE; - unsigned char _gpio = GPIO_NONE; + volatile unsigned long _counter = 0; + unsigned char _value = 0; + unsigned long _last = 0; + unsigned long _debounce = microsecondsToClockCycles(EVENTS1_DEBOUNCE * 1000); + bool _trigger = false; + bool _trigger_flag = false; + unsigned char _trigger_value = false; + + unsigned char _gpio = GPIO_NONE; unsigned char _pin_mode = INPUT; unsigned char _interrupt_mode = RISING; diff --git a/code/espurna/sensors/GeigerSensor.h b/code/espurna/sensors/GeigerSensor.h index 44dbbdba..42a00c72 100644 --- a/code/espurna/sensors/GeigerSensor.h +++ b/code/espurna/sensors/GeigerSensor.h @@ -138,8 +138,8 @@ double value(unsigned char index) { double value = _events * 60000; value = value / (_lastreport_cpm-_period_begin); #if SENSOR_DEBUG - char data[128]; char buffer[10]; - dtostrf(value, 1-sizeof(buffer), 4, buffer); + char data[128]; char buffer[32]; + dtostrf(value, 1, 4, buffer); snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | CPM: %s", _ticks, (_lastreport_cpm-_period_begin), buffer); DEBUG_MSG("[GEIGER] %s\n", data); #endif @@ -154,8 +154,8 @@ double value(unsigned char index) { double value = _ticks * 60000 / _cpm2sievert; value = value / (_lastreport_sv-_period_begin); #if SENSOR_DEBUG - char data[128]; char buffer[10]; - dtostrf(value, 1-sizeof(buffer), 4, buffer); + char data[128]; char buffer[32]; + dtostrf(value, 1, 4, buffer); snprintf(data, sizeof(data), "Ticks: %u | Interval: %u | µSievert: %s", _ticks, (_lastreport_sv-_period_begin), buffer); DEBUG_MSG("[GEIGER] %s\n", data); #endif diff --git a/code/espurna/sensors/HLW8012Sensor.h b/code/espurna/sensors/HLW8012Sensor.h index a6770698..dfca015e 100644 --- a/code/espurna/sensors/HLW8012Sensor.h +++ b/code/espurna/sensors/HLW8012Sensor.h @@ -158,7 +158,7 @@ class HLW8012Sensor : public BaseSensor { // Descriptive name of the sensor String description() { - char buffer[25]; + char buffer[28]; snprintf(buffer, sizeof(buffer), "HLW8012 @ GPIO(%u,%u,%u)", _sel, _cf, _cf1); return String(buffer); } @@ -170,7 +170,7 @@ class HLW8012Sensor : public BaseSensor { // Address of the sensor (it could be the GPIO or I2C address) String address(unsigned char index) { - char buffer[10]; + char buffer[12]; snprintf(buffer, sizeof(buffer), "%u:%u:%u", _sel, _cf, _cf1); return String(buffer); } diff --git a/code/espurna/sensors/PMSX003Sensor.h b/code/espurna/sensors/PMSX003Sensor.h index 040158ed..2e0526c7 100644 --- a/code/espurna/sensors/PMSX003Sensor.h +++ b/code/espurna/sensors/PMSX003Sensor.h @@ -276,8 +276,6 @@ class PMSX003Sensor : public BaseSensor, PMSX003 { return; } - _error = SENSOR_ERROR_OK; - #if PMS_SMART_SLEEP unsigned int readCycle; if (_readCount++ > 30) { @@ -304,22 +302,40 @@ class PMSX003Sensor : public BaseSensor, PMSX003 { uint16_t data[PMS_DATA_MAX]; if (readData(data, pms_specs[_type].data_count)) { if (_type == PMS_TYPE_5003ST) { - _slot_values[0] = data[4]; - _slot_values[1] = (double)data[13] / 10; - _slot_values[2] = (double)data[14] / 10; - _slot_values[3] = (double)data[12] / 1000; + if (data[14] > 10 && data[14] < 1000 && data[13] < 1000) { + _slot_values[0] = data[4]; + _slot_values[1] = (double)data[13] / 10; + _slot_values[2] = (double)data[14] / 10; + _slot_values[3] = (double)data[12] / 1000; + _error = SENSOR_ERROR_OK; + } else { + _error = SENSOR_ERROR_OUT_OF_RANGE; + #if SENSOR_DEBUG + DEBUG_MSG("[SENSOR] %s: Invalid temperature=%d humidity=%d.\n", pms_specs[_type].name, (int)data[13], (int)data[14]); + #endif + } } else if (_type == PMS_TYPE_5003S) { _slot_values[0] = data[4]; _slot_values[1] = data[5]; _slot_values[2] = (double)data[12] / 1000; + _error = SENSOR_ERROR_OK; } else if (_type == PMS_TYPE_5003T) { - _slot_values[0] = data[4]; - _slot_values[1] = (double)data[10] / 10; - _slot_values[2] = (double)data[11] / 10; + if (data[11] > 10 && data[11] < 1000 && data[10] < 1000) { + _slot_values[0] = data[4]; + _slot_values[1] = (double)data[10] / 10; + _slot_values[2] = (double)data[11] / 10; + _error = SENSOR_ERROR_OK; + } else { + _error = SENSOR_ERROR_OUT_OF_RANGE; + #if SENSOR_DEBUG + DEBUG_MSG("[SENSOR] %s: Invalid temperature=%d humidity=%d.\n", pms_specs[_type].name, (int)data[10], (int)data[11]); + #endif + } } else { _slot_values[0] = data[3]; _slot_values[1] = data[4]; _slot_values[2] = data[5]; + _error = SENSOR_ERROR_OK; } } diff --git a/code/espurna/sensors/PulseMeterSensor.h b/code/espurna/sensors/PulseMeterSensor.h index 7bb4d3d7..d3012890 100644 --- a/code/espurna/sensors/PulseMeterSensor.h +++ b/code/espurna/sensors/PulseMeterSensor.h @@ -43,6 +43,10 @@ class PulseMeterSensor : public BaseSensor { if (ratio > 0) _ratio = ratio; } + void setInterruptMode(unsigned char interrupt_mode) { + _interrupt_mode = interrupt_mode; + } + void setDebounceTime(unsigned long debounce) { _debounce = debounce; } @@ -57,6 +61,10 @@ class PulseMeterSensor : public BaseSensor { return _ratio; } + unsigned char getInterruptMode() { + return _interrupt_mode; + } + unsigned long getDebounceTime() { return _debounce; } @@ -144,7 +152,7 @@ class PulseMeterSensor : public BaseSensor { if (_gpio != _previous) { if (_previous != GPIO_NONE) _detach(_previous); - _attach(this, _gpio, PULSEMETER_INTERRUPT_ON); + _attach(this, _gpio, _interrupt_mode); _previous = _gpio; } @@ -171,6 +179,8 @@ class PulseMeterSensor : public BaseSensor { unsigned long _previous_pulses = 0; unsigned long _previous_time = 0; + unsigned char _interrupt_mode = FALLING; + }; diff --git a/code/espurna/sensors/SenseAirSensor.h b/code/espurna/sensors/SenseAirSensor.h index c73d25e3..f0dfe8e2 100644 --- a/code/espurna/sensors/SenseAirSensor.h +++ b/code/espurna/sensors/SenseAirSensor.h @@ -202,17 +202,17 @@ class SenseAirSensor : public BaseSensor, SenseAir { return; } - _error = SENSOR_ERROR_OK; - unsigned int co2 = readCo2(); if (co2 >= 5000 || co2 < 100) { _co2 = _lastCo2; + _error = SENSOR_ERROR_OUT_OF_RANGE; } else { _co2 = (co2 > _lastCo2 + 2000) ? _lastCo2 : co2; _lastCo2 = co2; + _error = SENSOR_ERROR_OK; } } diff --git a/code/espurna/settings.ino b/code/espurna/settings.ino index 00b00686..0e82b4d0 100644 --- a/code/espurna/settings.ino +++ b/code/espurna/settings.ino @@ -215,6 +215,23 @@ bool settingsRestoreJson(JsonObject& data) { } +bool settingsRestoreJson(char* json_string, size_t json_buffer_size = 1024) { + + // XXX: as of right now, arduinojson cannot trigger callbacks for each key individually + // Manually separating kv pairs can allow to parse only a small chunk, since we know that there is only string type used (even with bools / ints). Can be problematic when parsing data that was not generated by us. + // Current parsing method is limited only by keys (~sizeof(uintptr_t) bytes per key, data is not copied when string is non-const) + DynamicJsonBuffer jsonBuffer(json_buffer_size); + JsonObject& root = jsonBuffer.parseObject((char *) json_string); + + if (!root.success()) { + DEBUG_MSG_P(PSTR("[SETTINGS] JSON parsing error\n")); + return false; + } + + return settingsRestoreJson(root); + + } + void settingsGetJson(JsonObject& root) { // Get sorted list of keys @@ -228,6 +245,17 @@ void settingsGetJson(JsonObject& root) { } +void settingsProcessConfig(const settings_cfg_list_t& config, settings_filter_t filter) { + for (auto& entry : config) { + String value = getSetting(entry.key, entry.default_value); + if (filter) { + value = filter(value); + } + if (value.equals(entry.setting)) continue; + entry.setting = std::move(value); + } +} + // ----------------------------------------------------------------------------- // Initialization // ----------------------------------------------------------------------------- diff --git a/code/espurna/static/digicert_evroot_pem.h b/code/espurna/static/digicert_evroot_pem.h new file mode 100644 index 00000000..aa7b8c3d --- /dev/null +++ b/code/espurna/static/digicert_evroot_pem.h @@ -0,0 +1,42 @@ +// https://github.com root issuer + +// Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA +// Validity +// Not Before: Oct 22 12:00:00 2013 GMT +// Not After : Oct 22 12:00:00 2028 GMT +// Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 Extended Validation Server CA + +#pragma once + +#include + +const char PROGMEM _ssl_digicert_ev_root_ca[] = R"EOF( +-----BEGIN CERTIFICATE----- +MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW +YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY +uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/ +LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy +/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh +cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k +8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB +Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp +Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy +dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2 +MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j +b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW +gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh +hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg +4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa +2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs +1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1 +oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn +8TUoE6smftX3eg== +-----END CERTIFICATE----- +)EOF"; diff --git a/code/espurna/static/letsencrypt_isrgroot_pem.h b/code/espurna/static/letsencrypt_isrgroot_pem.h new file mode 100644 index 00000000..fe627af6 --- /dev/null +++ b/code/espurna/static/letsencrypt_isrgroot_pem.h @@ -0,0 +1,94 @@ +// ISRG Root X1 (self-signed) +// from https://letsencrypt.org/certs/isrgrootx1.pem.txt + +// Note: LetsEncrypt will only start using this root certificate to sign +// certificates after July 8, 2020. Any certificate issued before this date +// uses the X3 intermediate certificate down below. +// See: https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html + +// Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1 +// Validity +// Not Before: Jun 4 11:04:38 2015 GMT +// Not After : Jun 4 11:04:38 2035 GMT +// Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1 + +#pragma once + +#include + +const char PROGMEM _ssl_letsencrypt_isrg_x1_ca[] = R"EOF( +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +)EOF"; + + +// Let’s Encrypt Authority X3 (Signed by ISRG Root X1) +// from https://letsencrypt.org/certs/letsencryptauthorityx3.pem.txt + +// Issuer: C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 +// Validity +// Not Before: Oct 6 15:43:55 2016 GMT +// Not After : Oct 6 15:43:55 2021 GMT +const char PROGMEM _ssl_letsencrypt_isrg_x3_ca[] = R"EOF( +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1 +WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX +NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf +89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl +Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc +Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz +uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo +SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx +A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM +UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2 +DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1 +eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu +OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw +p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY +2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0 +ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR +PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b +rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt +-----END CERTIFICATE----- +)EOF"; \ No newline at end of file diff --git a/code/espurna/system.ino b/code/espurna/system.ino index 5853fb6d..2f90e5b3 100644 --- a/code/espurna/system.ino +++ b/code/espurna/system.ino @@ -163,8 +163,10 @@ void _systemSetupHeartbeat() { } #if WEB_SUPPORT - bool _systemWebSocketOnReceive(const char * key, JsonVariant& value) { - return (strncmp(key, "hb", 2) == 0); + bool _systemWebSocketOnKeyCheck(const char * key, JsonVariant& value) { + if (strncmp(key, "sys", 3) == 0) return true; + if (strncmp(key, "hb", 2) == 0) return true; + return false; } #endif @@ -259,7 +261,7 @@ void systemSetup() { #endif #if WEB_SUPPORT - wsOnReceiveRegister(_systemWebSocketOnReceive); + wsRegister().onKeyCheck(_systemWebSocketOnKeyCheck); #endif // Init device-specific hardware diff --git a/code/espurna/telnet.ino b/code/espurna/telnet.ino index b7ad357f..063575e2 100644 --- a/code/espurna/telnet.ino +++ b/code/espurna/telnet.ino @@ -10,10 +10,16 @@ Parts of the code have been borrowed from Thomas Sarlandie's NetServer #if TELNET_SUPPORT -#include +#if TELNET_SERVER == TELNET_SERVER_WIFISERVER + #include + WiFiServer _telnetServer = WiFiServer(TELNET_PORT); + std::unique_ptr _telnetClients[TELNET_MAX_CLIENTS]; +#else + #include + AsyncServer _telnetServer = AsyncServer(TELNET_PORT); + std::unique_ptr _telnetClients[TELNET_MAX_CLIENTS]; +#endif -AsyncServer * _telnetServer; -AsyncClient * _telnetClients[TELNET_MAX_CLIENTS]; bool _telnetFirst = true; bool _telnetAuth = TELNET_AUTHENTICATION; @@ -25,12 +31,11 @@ bool _telnetClientsAuth[TELNET_MAX_CLIENTS]; #if WEB_SUPPORT -bool _telnetWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _telnetWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "telnet", 6) == 0); } -void _telnetWebSocketOnSend(JsonObject& root) { - root["telnetVisible"] = 1; +void _telnetWebSocketOnConnected(JsonObject& root) { root["telnetSTA"] = getSetting("telnetSTA", TELNET_STA).toInt() == 1; root["telnetAuth"] = getSetting("telnetAuth", TELNET_AUTHENTICATION).toInt() == 1; } @@ -38,9 +43,11 @@ void _telnetWebSocketOnSend(JsonObject& root) { #endif void _telnetDisconnect(unsigned char clientId) { - _telnetClients[clientId]->free(); - delete _telnetClients[clientId]; - _telnetClients[clientId] = NULL; + // ref: we are called from onDisconnect, async is already stopped + #if TELNET_SERVER == TELNET_SERVER_WIFISERVER + _telnetClients[clientId]->stop(); + #endif + _telnetClients[clientId] = nullptr; wifiReconnectCheck(); DEBUG_MSG_P(PSTR("[TELNET] Client #%d disconnected\n"), clientId); } @@ -74,7 +81,6 @@ bool _telnetWrite(unsigned char clientId, const char * message) { } void _telnetData(unsigned char clientId, void *data, size_t len) { - // Skip first message since it's always garbage if (_telnetFirst) { _telnetFirst = false; @@ -87,13 +93,13 @@ void _telnetData(unsigned char clientId, void *data, size_t len) { // C-d is sent as two bytes (sometimes repeating) if (len >= 2) { if ((p[0] == 0xFF) && (p[1] == 0xEC)) { - _telnetClients[clientId]->close(true); + _telnetDisconnect(clientId); return; } } if ((strncmp(p, "close", 5) == 0) || (strncmp(p, "quit", 4) == 0)) { - _telnetClients[clientId]->close(); + _telnetDisconnect(clientId); return; } @@ -108,10 +114,10 @@ void _telnetData(unsigned char clientId, void *data, size_t len) { String password = getAdminPass(); if (strncmp(p, password.c_str(), password.length()) == 0) { DEBUG_MSG_P(PSTR("[TELNET] Client #%d authenticated\n"), clientId); - _telnetWrite(clientId, "Welcome!\n"); + _telnetWrite(clientId, "Password correct, welcome!\n"); _telnetClientsAuth[clientId] = true; } else { - _telnetWrite(clientId, "Password: "); + _telnetWrite(clientId, "Password (try again): "); } return; } @@ -120,13 +126,101 @@ void _telnetData(unsigned char clientId, void *data, size_t len) { #if TERMINAL_SUPPORT terminalInject(data, len); #endif +} + +void _telnetNotifyConnected(unsigned char i) { + + DEBUG_MSG_P(PSTR("[TELNET] Client #%u connected\n"), i); + + // If there is no terminal support automatically dump info and crash data + #if TERMINAL_SUPPORT == 0 + info(); + wifiDebug(); + crashDump(); + crashClear(); + #endif + + #ifdef ESPURNA_CORE + _telnetClientsAuth[i] = true; + #else + _telnetClientsAuth[i] = !_telnetAuth; + if (_telnetAuth) { + if (getAdminPass().length()) { + _telnetWrite(i, "Password: "); + } else { + _telnetClientsAuth[i] = true; + } + } + #endif + + _telnetFirst = true; + wifiReconnectCheck(); } -void _telnetNewClient(AsyncClient *client) { +#if TELNET_SERVER == TELNET_SERVER_WIFISERVER - if (client->localIP() != WiFi.softAPIP()) { +void _telnetLoop() { + if (_telnetServer.hasClient()) { + int i; + + for (i = 0; i < TELNET_MAX_CLIENTS; i++) { + if (!_telnetClients[i] || !_telnetClients[i]->connected()) { + + _telnetClients[i] = std::unique_ptr(new WiFiClient(_telnetServer.available())); + + if (_telnetClients[i]->localIP() != WiFi.softAPIP()) { + // Telnet is always available for the ESPurna Core image + #ifdef ESPURNA_CORE + bool telnetSTA = true; + #else + bool telnetSTA = getSetting("telnetSTA", TELNET_STA).toInt() == 1; + #endif + + if (!telnetSTA) { + DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n")); + _telnetDisconnect(i); + return; + } + } + + _telnetNotifyConnected(i); + + break; + } + } + //no free/disconnected spot so reject + if (i == TELNET_MAX_CLIENTS) { + DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n")); + _telnetServer.available().stop(); + return; + } + } + + for (int i = 0; i < TELNET_MAX_CLIENTS; i++) { + if (_telnetClients[i]) { + // Handle client timeouts + if (!_telnetClients[i]->connected()) { + _telnetDisconnect(i); + } else { + // Read data from clients + while (_telnetClients[i] && _telnetClients[i]->available()) { + char data[TERMINAL_BUFFER_SIZE]; + size_t len = _telnetClients[i]->available(); + unsigned int r = _telnetClients[i]->readBytes(data, min(sizeof(data), len)); + + _telnetData(i, data, r); + } + } + } + } +} + +#else // TELNET_SERVER_ASYNC + +void _telnetNewClient(AsyncClient* client) { + if (client->localIP() != WiFi.softAPIP()) { // Telnet is always available for the ESPurna Core image #ifdef ESPURNA_CORE bool telnetSTA = true; @@ -137,76 +231,54 @@ void _telnetNewClient(AsyncClient *client) { if (!telnetSTA) { DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Only local connections\n")); client->onDisconnect([](void *s, AsyncClient *c) { - c->free(); delete c; }); client->close(true); return; } - } for (unsigned char i = 0; i < TELNET_MAX_CLIENTS; i++) { if (!_telnetClients[i] || !_telnetClients[i]->connected()) { - _telnetClients[i] = client; + _telnetClients[i] = std::unique_ptr(client); - client->onAck([i](void *s, AsyncClient *c, size_t len, uint32_t time) { + _telnetClients[i]->onAck([i](void *s, AsyncClient *c, size_t len, uint32_t time) { }, 0); - client->onData([i](void *s, AsyncClient *c, void *data, size_t len) { + _telnetClients[i]->onData([i](void *s, AsyncClient *c, void *data, size_t len) { _telnetData(i, data, len); }, 0); - client->onDisconnect([i](void *s, AsyncClient *c) { + _telnetClients[i]->onDisconnect([i](void *s, AsyncClient *c) { _telnetDisconnect(i); }, 0); - client->onError([i](void *s, AsyncClient *c, int8_t error) { + _telnetClients[i]->onError([i](void *s, AsyncClient *c, int8_t error) { DEBUG_MSG_P(PSTR("[TELNET] Error %s (%d) on client #%u\n"), c->errorToString(error), error, i); }, 0); - client->onTimeout([i](void *s, AsyncClient *c, uint32_t time) { + _telnetClients[i]->onTimeout([i](void *s, AsyncClient *c, uint32_t time) { DEBUG_MSG_P(PSTR("[TELNET] Timeout on client #%u at %lu\n"), i, time); c->close(); }, 0); - DEBUG_MSG_P(PSTR("[TELNET] Client #%u connected\n"), i); - - // If there is no terminal support automatically dump info and crash data - #if TERMINAL_SUPPORT == 0 - info(); - wifiDebug(); - crashDump(); - crashClear(); - #endif - - #ifdef ESPURNA_CORE - _telnetClientsAuth[i] = true; - #else - _telnetClientsAuth[i] = !_telnetAuth; - if (_telnetAuth) _telnetWrite(i, "Password: "); - #endif - - _telnetFirst = true; - wifiReconnectCheck(); - + _telnetNotifyConnected(i); return; - } } DEBUG_MSG_P(PSTR("[TELNET] Rejecting - Too many connections\n")); client->onDisconnect([](void *s, AsyncClient *c) { - c->free(); delete c; }); client->close(true); - } +#endif // TELNET_SERVER == TELNET_SERVER_WIFISERVER + // ----------------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------------- @@ -228,22 +300,30 @@ void _telnetConfigure() { } void telnetSetup() { - - _telnetServer = new AsyncServer(TELNET_PORT); - _telnetServer->onClient([](void *s, AsyncClient* c) { - _telnetNewClient(c); - }, 0); - _telnetServer->begin(); + #if TELNET_SERVER == TELNET_SERVER_WIFISERVER + espurnaRegisterLoop(_telnetLoop); + _telnetServer.setNoDelay(true); + _telnetServer.begin(); + #else + _telnetServer.onClient([](void *s, AsyncClient* c) { + _telnetNewClient(c); + }, 0); + _telnetServer.begin(); + #endif #if WEB_SUPPORT - wsOnSendRegister(_telnetWebSocketOnSend); - wsOnReceiveRegister(_telnetWebSocketOnReceive); + wsRegister() + .onVisible([](JsonObject& root) { root["telnetVisible"] = 1; }) + .onConnected(_telnetWebSocketOnConnected) + .onKeyCheck(_telnetWebSocketOnKeyCheck); #endif espurnaRegisterReload(_telnetConfigure); _telnetConfigure(); - DEBUG_MSG_P(PSTR("[TELNET] Listening on port %d\n"), TELNET_PORT); + DEBUG_MSG_P(PSTR("[TELNET] %s server, Listening on port %d\n"), + (TELNET_SERVER == TELNET_SERVER_WIFISERVER) ? "Sync" : "Async", + TELNET_PORT); } diff --git a/code/espurna/terminal.ino b/code/espurna/terminal.ino index c67d6fd6..d57818ab 100644 --- a/code/espurna/terminal.ino +++ b/code/espurna/terminal.ino @@ -12,6 +12,7 @@ Copyright (C) 2016-2019 by Xose Pérez #include "libs/EmbedisWrap.h" #include #include "libs/StreamInjector.h" +#include "libs/HeapStats.h" StreamInjector _serial = StreamInjector(TERMINAL_BUFFER_SIZE); EmbedisWrap embedis(_serial, TERMINAL_BUFFER_SIZE); @@ -78,15 +79,92 @@ void _terminalKeysCommand() { } -void _terminalInitCommand() { +#if LWIP_VERSION_MAJOR != 1 + +// not yet CONNECTING or LISTENING +extern struct tcp_pcb *tcp_bound_pcbs; +// accepting or sending data +extern struct tcp_pcb *tcp_active_pcbs; +// // TIME-WAIT status +extern struct tcp_pcb *tcp_tw_pcbs; + +String _terminalPcbStateToString(const unsigned char state) { + switch (state) { + case 0: return F("CLOSED"); + case 1: return F("LISTEN"); + case 2: return F("SYN_SENT"); + case 3: return F("SYN_RCVD"); + case 4: return F("ESTABLISHED"); + case 5: return F("FIN_WAIT_1"); + case 6: return F("FIN_WAIT_2"); + case 7: return F("CLOSE_WAIT"); + case 8: return F("CLOSING"); + case 9: return F("LAST_ACK"); + case 10: return F("TIME_WAIT"); + default: return String(int(state)); + }; +} - #if DEBUG_SUPPORT - terminalRegisterCommand(F("CRASH"), [](Embedis* e) { - crashDump(); - crashClear(); - terminalOK(); - }); +void _terminalPrintTcpPcb(tcp_pcb* pcb) { + + char remote_ip[32] = {0}; + char local_ip[32] = {0}; + + inet_ntoa_r((pcb->local_ip), local_ip, sizeof(local_ip)); + inet_ntoa_r((pcb->remote_ip), remote_ip, sizeof(remote_ip)); + + DEBUG_MSG_P(PSTR("state=%s local=%s:%u remote=%s:%u snd_queuelen=%u lastack=%u send_wnd=%u rto=%u\n"), + _terminalPcbStateToString(pcb->state).c_str(), + local_ip, pcb->local_port, + remote_ip, pcb->remote_port, + pcb->snd_queuelen, pcb->lastack, + pcb->snd_wnd, pcb->rto + ); + +} + +void _terminalPrintTcpPcbs() { + + tcp_pcb *pcb; + //DEBUG_MSG_P(PSTR("Active PCB states:\n")); + for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) { + _terminalPrintTcpPcb(pcb); + } + //DEBUG_MSG_P(PSTR("TIME-WAIT PCB states:\n")); + for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) { + _terminalPrintTcpPcb(pcb); + } + //DEBUG_MSG_P(PSTR("BOUND PCB states:\n")); + for (pcb = tcp_bound_pcbs; pcb != NULL; pcb = pcb->next) { + _terminalPrintTcpPcb(pcb); + } + +} + +void _terminalPrintDnsResult(const char* name, const ip_addr_t* address) { + // TODO fix asynctcp building with lwip-ipv6 + /* + #if LWIP_IPV6 + if (IP_IS_V6(address)) { + DEBUG_MSG_P(PSTR("[DNS] %s has IPV6 address %s\n"), name, ip6addr_ntoa(ip_2_ip6(address))); + } #endif + */ + DEBUG_MSG_P(PSTR("[DNS] %s has address %s\n"), name, ipaddr_ntoa(address)); +} + +void _terminalDnsFound(const char* name, const ip_addr_t* result, void*) { + if (!result) { + DEBUG_MSG_P(PSTR("[DNS] %s not found\n"), name); + return; + } + + _terminalPrintDnsResult(name, result); +} + +#endif // LWIP_VERSION_MAJOR != 1 + +void _terminalInitCommand() { terminalRegisterCommand(F("COMMANDS"), [](Embedis* e) { _terminalHelpCommand(); @@ -106,30 +184,39 @@ void _terminalInitCommand() { }); terminalRegisterCommand(F("GPIO"), [](Embedis* e) { + int pin = -1; + if (e->argc < 2) { - terminalError(F("Wrong arguments")); - return; + DEBUG_MSG("Printing all GPIO pins:\n"); + } else { + pin = String(e->argv[1]).toInt(); + if (!gpioValid(pin)) { + terminalError(F("Invalid GPIO pin")); + return; + } + + if (e->argc > 2) { + bool state = String(e->argv[2]).toInt() == 1; + digitalWrite(pin, state); + } } - int pin = String(e->argv[1]).toInt(); - //if (!gpioValid(pin)) { - // terminalError(F("Invalid GPIO")); - // return; - //} - if (e->argc > 2) { - bool state = String(e->argv[2]).toInt() == 1; - digitalWrite(pin, state); + + for (int i = 0; i <= 15; i++) { + if (gpioValid(i) && (pin == -1 || pin == i)) { + DEBUG_MSG_P(PSTR("GPIO %s pin %d is %s\n"), GPEP(i) ? "output" : "input", i, digitalRead(i) == HIGH ? "HIGH" : "LOW"); + } } - DEBUG_MSG_P(PSTR("GPIO %d is %s\n"), pin, digitalRead(pin) == HIGH ? "HIGH" : "LOW"); + terminalOK(); }); terminalRegisterCommand(F("HEAP"), [](Embedis* e) { - infoMemory("Heap", getInitialFreeHeap(), getFreeHeap()); + infoHeapStats(); terminalOK(); }); terminalRegisterCommand(F("STACK"), [](Embedis* e) { - infoMemory("Stack", 4096, getFreeStack()); + infoMemory("Stack", CONT_STACKSIZE, getFreeStack()); terminalOK(); }); @@ -190,9 +277,10 @@ void _terminalInitCommand() { }); terminalRegisterCommand(F("CONFIG"), [](Embedis* e) { - DynamicJsonBuffer jsonBuffer; + DynamicJsonBuffer jsonBuffer(1024); JsonObject& root = jsonBuffer.createObject(); settingsGetJson(root); + // XXX: replace with streaming String output; root.printTo(output); DEBUG_MSG(output.c_str()); @@ -202,9 +290,56 @@ void _terminalInitCommand() { #if not SETTINGS_AUTOSAVE terminalRegisterCommand(F("SAVE"), [](Embedis* e) { eepromCommit(); - DEBUG_MSG_P(PSTR("\n+OK\n")); + terminalOK(); + }); + #endif + + #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL + terminalRegisterCommand(F("MFLN.PROBE"), [](Embedis* e) { + if (e->argc != 3) { + terminalError(F("[url] [value]")); + return; + } + + URL _url(e->argv[1]); + uint16_t requested_mfln = atol(e->argv[2]); + + auto client = std::make_unique(); + client->setInsecure(); + + if (client->probeMaxFragmentLength(_url.host.c_str(), _url.port, requested_mfln)) { + terminalOK(); + } else { + terminalError(F("Buffer size not supported")); + } }); #endif + + #if LWIP_VERSION_MAJOR != 1 + terminalRegisterCommand(F("HOST"), [](Embedis* e) { + if (e->argc != 2) { + terminalError(F("HOST [hostname]")); + return; + } + + ip_addr_t result; + auto error = dns_gethostbyname(e->argv[1], &result, _terminalDnsFound, nullptr); + if (error == ERR_OK) { + _terminalPrintDnsResult(e->argv[1], &result); + terminalOK(); + return; + } else if (error != ERR_INPROGRESS) { + DEBUG_MSG_P(PSTR("[DNS] dns_gethostbyname error: %s\n"), lwip_strerr(error)); + return; + } + + }); + + terminalRegisterCommand(F("NETSTAT"), [](Embedis*) { + _terminalPrintTcpPcbs(); + }); + + #endif // LWIP_VERSION_MAJOR != 1 } @@ -241,6 +376,11 @@ void terminalInject(void *data, size_t len) { _serial.inject((char *) data, len); } +void terminalInject(char ch) { + _serial.inject(ch); +} + + Stream & terminalSerial() { return (Stream &) _serial; } @@ -268,6 +408,11 @@ void terminalSetup() { #endif }); + #if WEB_SUPPORT + wsRegister() + .onVisible([](JsonObject& root) { root["cmdVisible"] = 1; }); + #endif + _terminalInitCommand(); #if SERIAL_RX_ENABLED diff --git a/code/espurna/thermostat.ino b/code/espurna/thermostat.ino index cc9be25d..4ad5c543 100644 --- a/code/espurna/thermostat.ino +++ b/code/espurna/thermostat.ino @@ -142,13 +142,13 @@ void updateOperationMode() { //------------------------------------------------------------------------------ void updateRemoteTemp(bool remote_temp_actual) { #if WEB_SUPPORT - char tmp_str[6]; + char tmp_str[16]; if (remote_temp_actual) { - dtostrf(_remote_temp.temp, 1-sizeof(tmp_str), 1, tmp_str); + dtostrf(_remote_temp.temp, 1, 1, tmp_str); } else { strcpy(tmp_str, "\"?\""); } - char buffer[100]; + char buffer[128]; snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str); wsSend(buffer); #endif @@ -298,7 +298,7 @@ void _thermostatReload() { #if WEB_SUPPORT //------------------------------------------------------------------------------ -void _thermostatWebSocketOnSend(JsonObject& root) { +void _thermostatWebSocketOnConnected(JsonObject& root) { root["thermostatEnabled"] = thermostatEnabled(); root["thermostatMode"] = thermostatModeCooler(); root["thermostatVisible"] = 1; @@ -328,7 +328,7 @@ void _thermostatWebSocketOnSend(JsonObject& root) { } //------------------------------------------------------------------------------ -bool _thermostatWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _thermostatWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, NAME_THERMOSTAT_ENABLED, strlen(NAME_THERMOSTAT_ENABLED)) == 0) return true; if (strncmp(key, NAME_THERMOSTAT_MODE, strlen(NAME_THERMOSTAT_MODE)) == 0) return true; if (strncmp(key, NAME_TEMP_RANGE_MIN, strlen(NAME_TEMP_RANGE_MIN)) == 0) return true; @@ -358,9 +358,10 @@ void thermostatSetup() { // Websockets #if WEB_SUPPORT - wsOnSendRegister(_thermostatWebSocketOnSend); - wsOnReceiveRegister(_thermostatWebSocketOnReceive); - wsOnActionRegister(_thermostatWebSocketOnAction); + wsRegister() + .onConnected(_thermostatWebSocketOnConnected) + .onKeyCheck(_thermostatWebSocketOnKeyCheck) + .onAction(_thermostatWebSocketOnAction); #endif espurnaRegisterLoop(thermostatLoop); @@ -386,8 +387,8 @@ void setThermostatState(bool state) { //------------------------------------------------------------------------------ void debugPrintSwitch(bool state, double temp) { - char tmp_str[6]; - dtostrf(temp, 1-sizeof(tmp_str), 1, tmp_str); + char tmp_str[16]; + dtostrf(temp, 1, 1, tmp_str); DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"), state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, _thermostat_mode_cooler ? "COOLER" : "HEATER", relayStatus(THERMOSTAT_RELAY) ? "ON" : "OFF", millis() - _thermostat.last_switch); } @@ -485,8 +486,8 @@ double getLocalTemperature() { for (byte i=0; i -0.1 && temp < 0.1 ? DBL_MIN : temp; } @@ -501,8 +502,8 @@ double getLocalHumidity() { for (byte i=0; i -0.1 && hum < 0.1 ? DBL_MIN : hum; } @@ -822,4 +823,4 @@ void displayLoop() { } } -#endif // THERMOSTAT_DISPLAY_SUPPORT \ No newline at end of file +#endif // THERMOSTAT_DISPLAY_SUPPORT diff --git a/code/espurna/thinkspeak.ino b/code/espurna/thinkspeak.ino index f6cb70cc..c53cb303 100644 --- a/code/espurna/thinkspeak.ino +++ b/code/espurna/thinkspeak.ino @@ -10,28 +10,35 @@ Copyright (C) 2019 by Xose Pérez #if THINGSPEAK_USE_ASYNC #include -AsyncClient * _tspk_client; #else #include #endif +#define THINGSPEAK_DATA_BUFFER_SIZE 256 + const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM = "POST %s HTTP/1.1\r\n" "Host: %s\r\n" "User-Agent: ESPurna\r\n" "Connection: close\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" - "Content-Length: %d\r\n\r\n" - "%s\r\n"; + "Content-Length: %d\r\n\r\n"; bool _tspk_enabled = false; bool _tspk_clear = false; char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL}; +String _tspk_data; bool _tspk_flush = false; unsigned long _tspk_last_flush = 0; -unsigned char _tspk_tries = 0; +unsigned char _tspk_tries = THINGSPEAK_TRIES; + +#if THINGSPEAK_USE_ASYNC +AsyncClient * _tspk_client; +bool _tspk_connecting = false; +bool _tspk_connected = false; +#endif // ----------------------------------------------------------------------------- @@ -56,13 +63,15 @@ void _tspkBrokerCallback(const unsigned char type, const char * topic, unsigned #if WEB_SUPPORT -bool _tspkWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _tspkWebSocketOnKeyCheck(const char * key, JsonVariant& value) { return (strncmp(key, "tspk", 4) == 0); } -void _tspkWebSocketOnSend(JsonObject& root) { +void _tspkWebSocketOnVisible(JsonObject& root) { + root["tspkVisible"] = static_cast(haveRelaysOrSensors()); +} - unsigned char visible = 0; +void _tspkWebSocketOnConnected(JsonObject& root) { root["tspkEnabled"] = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1; root["tspkKey"] = getSetting("tspkKey"); @@ -72,15 +81,11 @@ void _tspkWebSocketOnSend(JsonObject& root) { for (byte i=0; i 0) visible = 1; #if SENSOR_SUPPORT _sensorWebSocketMagnitudes(root, "tspk"); - visible = visible || (magnitudeCount() > 0); #endif - root["tspkVisible"] = visible; - } #endif @@ -92,50 +97,106 @@ void _tspkConfigure() { _tspk_enabled = false; setSetting("tspkEnabled", 0); } + if (_tspk_enabled && !_tspk_client) _tspkInitClient(); } #if THINGSPEAK_USE_ASYNC -void _tspkPost(String data) { +enum class tspk_state_t : uint8_t { + NONE, + HEADERS, + BODY +}; - if (_tspk_client == NULL) { - _tspk_client = new AsyncClient(); - } - - _tspk_client->onDisconnect([](void *s, AsyncClient *c) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); - _tspk_client->free(); - delete _tspk_client; - _tspk_client = NULL; - }, 0); +tspk_state_t _tspk_client_state = tspk_state_t::NONE; +unsigned long _tspk_client_ts = 0; +constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000; - _tspk_client->onTimeout([](void *s, AsyncClient *c, uint32_t time) { - _tspk_client->close(true); - }, 0); +void _tspkInitClient() { - _tspk_client->onData([](void * arg, AsyncClient * c, void * response, size_t len) { - - char * b = (char *) response; - b[len] = 0; - char * p = strstr((char *)response, "\r\n\r\n"); - unsigned int code = (p != NULL) ? atoi(&p[4]) : 0; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code); + _tspk_client = new AsyncClient(); + _tspk_client->onDisconnect([](void * s, AsyncClient * client) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n")); + _tspk_data = ""; + _tspk_client_ts = 0; _tspk_last_flush = millis(); - if ((0 == code) && (--_tspk_tries > 0)) { - _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n")); - } else { - _tspkClearQueue(); + _tspk_connected = false; + _tspk_connecting = false; + _tspk_client_state = tspk_state_t::NONE; + }, nullptr); + + _tspk_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Network timeout after %ums\n"), time); + client->close(true); + }, nullptr); + + _tspk_client->onPoll([](void * s, AsyncClient * client) { + uint32_t ts = millis() - _tspk_client_ts; + if (ts > THINGSPEAK_CLIENT_TIMEOUT) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] No response after %ums\n"), ts); + client->close(true); } + }, nullptr); + + _tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) { + + char * p = nullptr; + + do { + + p = nullptr; + + switch (_tspk_client_state) { + case tspk_state_t::NONE: + { + p = strnstr(reinterpret_cast(response), "HTTP/1.1 200 OK", len); + if (!p) { + client->close(true); + return; + } + _tspk_client_state = tspk_state_t::HEADERS; + continue; + } + case tspk_state_t::HEADERS: + { + p = strnstr(reinterpret_cast(response), "\r\n\r\n", len); + if (!p) return; + _tspk_client_state = tspk_state_t::BODY; + } + case tspk_state_t::BODY: + { + if (!p) { + p = strnstr(reinterpret_cast(response), "\r\n\r\n", len); + if (!p) return; + } + + unsigned int code = (p) ? atoi(&p[4]) : 0; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); + + if ((0 == code) && _tspk_tries) { + _tspk_flush = true; + DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); + } else { + _tspkClearQueue(); + } + + client->close(true); + + _tspk_client_state = tspk_state_t::NONE; + } + } - _tspk_client->close(true); + } while (_tspk_client_state != tspk_state_t::NONE); - }, NULL); + }, nullptr); - _tspk_client->onConnect([data](void * arg, AsyncClient * client) { + _tspk_client->onConnect([](void * arg, AsyncClient * client) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%d\n"), THINGSPEAK_HOST, THINGSPEAK_PORT); + _tspk_connected = true; + _tspk_connecting = false; + + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT); #if THINGSPEAK_USE_SSL uint8_t fp[20] = {0}; @@ -146,27 +207,36 @@ void _tspkPost(String data) { } #endif - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str()); - - char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()]; - snprintf_P(buffer, sizeof(buffer), + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str()); + char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1]; + snprintf_P(headers, sizeof(headers), THINGSPEAK_REQUEST_TEMPLATE, THINGSPEAK_URL, THINGSPEAK_HOST, - data.length(), - data.c_str() + _tspk_data.length() ); - client->write(buffer); + client->write(headers); + client->write(_tspk_data.c_str()); - }, NULL); + }, nullptr); + +} - #if ASYNC_TCP_SSL_ENABLED +void _tspkPost() { + + if (_tspk_connected || _tspk_connecting) return; + + _tspk_client_ts = millis(); + + #if SECURE_CLIENT == SECURE_CLIENT_AXTLS bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL); #else bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT); #endif + _tspk_connecting = connected; + if (!connected) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n")); _tspk_client->close(true); @@ -176,7 +246,7 @@ void _tspkPost(String data) { #else // THINGSPEAK_USE_ASYNC -void _tspkPost(String data) { +void _tspkPost() { #if THINGSPEAK_USE_SSL WiFiClientSecure _tspk_client; @@ -186,35 +256,36 @@ void _tspkPost(String data) { if (_tspk_client.connect(THINGSPEAK_HOST, THINGSPEAK_PORT)) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%d\n"), THINGSPEAK_HOST, THINGSPEAK_PORT); + DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT); if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, THINGSPEAK_HOST)) { DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n")); } - DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, data.c_str()); - char buffer[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + data.length()]; - snprintf_P(buffer, sizeof(buffer), + DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str()); + char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1]; + snprintf_P(headers, sizeof(headers), THINGSPEAK_REQUEST_TEMPLATE, THINGSPEAK_URL, THINGSPEAK_HOST, - data.length(), - data.c_str() + _tspk_data.length() ); - _tspk_client.print(buffer); + + _tspk_client.print(headers); + _tspk_client.print(_tspk_data); nice_delay(100); String response = _tspk_client.readString(); int pos = response.indexOf("\r\n\r\n"); unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %d\n"), code); + DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code); _tspk_client.stop(); _tspk_last_flush = millis(); - if ((0 == code) && (--_tspk_tries > 0)) { + if ((0 == code) && _tspk_tries) { _tspk_flush = true; - DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing\n")); + DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries); } else { _tspkClearQueue(); } @@ -229,14 +300,15 @@ void _tspkPost(String data) { #endif // THINGSPEAK_USE_ASYNC -void _tspkEnqueue(unsigned char index, char * payload) { - DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%d with value %s\n"), index, payload); +void _tspkEnqueue(unsigned char index, const char * payload) { + DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload); --index; if (_tspk_queue[index] != NULL) free(_tspk_queue[index]); _tspk_queue[index] = strdup(payload); } void _tspkClearQueue() { + _tspk_tries = THINGSPEAK_TRIES; if (_tspk_clear) { for (unsigned char id=0; id 0) data = data + String("&"); - data = data + String("field") + String(id+1) + String("=") + String(_tspk_queue[id]); + if (_tspk_data.length() > 0) _tspk_data.concat("&"); + char buf[32] = {0}; + snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]); + _tspk_data.concat(buf); } } // POST data if any - if (data.length() > 0) { - data = data + String("&api_key=") + getSetting("tspkKey"); - _tspk_tries = THINGSPEAK_TRIES; - _tspkPost(data); + if (_tspk_data.length()) { + _tspk_data.concat("&api_key="); + _tspk_data.concat(getSetting("tspkKey")); + --_tspk_tries; + _tspkPost(); } } @@ -281,7 +361,7 @@ bool tspkEnqueueRelay(unsigned char index, char * payload) { return false; } -bool tspkEnqueueMeasurement(unsigned char index, char * payload) { +bool tspkEnqueueMeasurement(unsigned char index, const char * payload) { if (!_tspk_enabled) return true; unsigned char id = getSetting("tspkMagnitude", index, 0).toInt(); if (id > 0) { @@ -304,8 +384,10 @@ void tspkSetup() { _tspkConfigure(); #if WEB_SUPPORT - wsOnSendRegister(_tspkWebSocketOnSend); - wsOnReceiveRegister(_tspkWebSocketOnReceive); + wsRegister() + .onVisible(_tspkWebSocketOnVisible) + .onConnected(_tspkWebSocketOnConnected) + .onKeyCheck(_tspkWebSocketOnKeyCheck); #endif #if BROKER_SUPPORT @@ -326,9 +408,7 @@ void tspkSetup() { void tspkLoop() { if (!_tspk_enabled) return; if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return; - if (_tspk_flush && (millis() - _tspk_last_flush > THINGSPEAK_MIN_INTERVAL)) { - _tspkFlush(); - } + _tspkFlush(); } #endif diff --git a/code/espurna/utils.ino b/code/espurna/utils.ino index 11e6f974..e95d7d75 100644 --- a/code/espurna/utils.ino +++ b/code/espurna/utils.ino @@ -7,6 +7,7 @@ Copyright (C) 2017-2019 by Xose Pérez */ #include +#include "libs/HeapStats.h" String getIdentifier() { char buffer[20]; @@ -49,7 +50,7 @@ String getCoreVersion() { String getCoreRevision() { #ifdef ARDUINO_ESP8266_GIT_VER - return String(ARDUINO_ESP8266_GIT_VER); + return String(ARDUINO_ESP8266_GIT_VER, 16); #else return String(""); #endif @@ -63,30 +64,14 @@ unsigned char getHeartbeatInterval() { return getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt(); } -// WTF -// Calling ESP.getFreeHeap() is making the system crash on a specific -// AiLight bulb, but anywhere else... -unsigned int getFreeHeap() { - if (getSetting("wtfHeap", 0).toInt() == 1) return 9999; - return ESP.getFreeHeap(); -} - -unsigned int getInitialFreeHeap() { - static unsigned int _heap = 0; - if (0 == _heap) { - _heap = getFreeHeap(); - } - return _heap; -} - -unsigned int getUsedHeap() { - return getInitialFreeHeap() - getFreeHeap(); -} - String getEspurnaModules() { return FPSTR(espurna_modules); } +String getEspurnaOTAModules() { + return FPSTR(espurna_ota_modules); +} + #if SENSOR_SUPPORT String getEspurnaSensors() { return FPSTR(espurna_sensors); @@ -98,35 +83,19 @@ String getEspurnaWebUI() { } String buildTime() { - - const char time_now[] = __TIME__; // hh:mm:ss - unsigned int hour = atoi(&time_now[0]); - unsigned int minute = atoi(&time_now[3]); - unsigned int second = atoi(&time_now[6]); - - const char date_now[] = __DATE__; // Mmm dd yyyy - const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - unsigned int month = 0; - for ( int i = 0; i < 12; i++ ) { - if (strncmp(date_now, months[i], 3) == 0 ) { - month = i + 1; - break; - } - } - unsigned int day = atoi(&date_now[3]); - unsigned int year = atoi(&date_now[7]); - - char buffer[20]; - snprintf_P( - buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), - year, month, day, hour, minute, second - ); - - return String(buffer); - + #if NTP_SUPPORT + return ntpDateTime(__UNIX_TIMESTAMP__); + #else + char buffer[20]; + 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 } - unsigned long getUptime() { static unsigned long last_uptime = 0; @@ -140,6 +109,15 @@ unsigned long getUptime() { } +bool haveRelaysOrSensors() { + bool result = false; + result = (relayCount() > 0); + #if SENSOR_SUPPORT + result = result || (magnitudeCount() > 0); + #endif + return result; +} + // ----------------------------------------------------------------------------- // Heartbeat helper // ----------------------------------------------------------------------------- @@ -202,10 +180,10 @@ namespace Heartbeat { void heartbeat() { unsigned long uptime_seconds = getUptime(); - unsigned int free_heap = getFreeHeap(); - + heap_stats_t heap_stats = getHeapStats(); + UNUSED(uptime_seconds); - UNUSED(free_heap); + UNUSED(heap_stats); #if MQTT_SUPPORT unsigned char _heartbeat_mode = getHeartbeatMode(); @@ -220,7 +198,7 @@ void heartbeat() { if (serial) { DEBUG_MSG_P(PSTR("[MAIN] Uptime: %lu seconds\n"), uptime_seconds); - infoMemory("Heap", getInitialFreeHeap(), getFreeHeap()); + infoHeapStats(); #if ADC_MODE_VALUE == ADC_VCC DEBUG_MSG_P(PSTR("[MAIN] Power: %lu mV\n"), ESP.getVcc()); #endif @@ -280,7 +258,7 @@ void heartbeat() { #endif if (hb_cfg & Heartbeat::Freeheap) - mqttSend(MQTT_TOPIC_FREEHEAP, String(free_heap).c_str()); + mqttSend(MQTT_TOPIC_FREEHEAP, String(heap_stats.available).c_str()); if (hb_cfg & Heartbeat::Relay) relayMQTT(); @@ -294,7 +272,7 @@ void heartbeat() { mqttSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str()); if (hb_cfg & Heartbeat::Status) - mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true); + mqttSendStatus(); if (hb_cfg & Heartbeat::Loadavg) mqttSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str()); @@ -306,14 +284,14 @@ void heartbeat() { } if (hb_cfg & Heartbeat::Remote_temp) { - char remote_temp[6]; - dtostrf(_remote_temp.temp, 1-sizeof(remote_temp), 1, remote_temp); - mqttSend(MQTT_TOPIC_REMOTE_TEMP, String(remote_temp).c_str()); + char remote_temp[16]; + dtostrf(_remote_temp.temp, 1, 1, remote_temp); + mqttSend(MQTT_TOPIC_REMOTE_TEMP, remote_temp); } #endif } else if (!serial && _heartbeat_mode == HEARTBEAT_REPEAT_STATUS) { - mqttSend(MQTT_TOPIC_STATUS, MQTT_STATUS_ONLINE, true); + mqttSendStatus(); } #endif @@ -327,7 +305,7 @@ void heartbeat() { idbSend(MQTT_TOPIC_UPTIME, String(uptime_seconds).c_str()); if (hb_cfg & Heartbeat::Freeheap) - idbSend(MQTT_TOPIC_FREEHEAP, String(free_heap).c_str()); + idbSend(MQTT_TOPIC_FREEHEAP, String(heap_stats.available).c_str()); if (hb_cfg & Heartbeat::Rssi) idbSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str()); @@ -422,6 +400,7 @@ void info() { DEBUG_MSG_P(PSTR("[MAIN] SDK version: %s\n"), ESP.getSdkVersion()); DEBUG_MSG_P(PSTR("[MAIN] Core version: %s\n"), getCoreVersion().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Core revision: %s\n"), getCoreRevision().c_str()); + DEBUG_MSG_P(PSTR("[MAIN] Build time: %lu\n"), __UNIX_TIMESTAMP__); DEBUG_MSG_P(PSTR("\n")); // ------------------------------------------------------------------------- @@ -470,11 +449,15 @@ void info() { // ------------------------------------------------------------------------- + static bool show_frag_stats = false; + infoMemory("EEPROM", SPI_FLASH_SEC_SIZE, SPI_FLASH_SEC_SIZE - settingsSize()); - infoMemory("Heap", getInitialFreeHeap(), getFreeHeap()); - infoMemory("Stack", 4096, getFreeStack()); + infoHeapStats(show_frag_stats); + infoMemory("Stack", CONT_STACKSIZE, getFreeStack()); DEBUG_MSG_P(PSTR("\n")); + show_frag_stats = true; + // ------------------------------------------------------------------------- DEBUG_MSG_P(PSTR("[MAIN] Boot version: %d\n"), ESP.getBootVersion()); @@ -494,6 +477,7 @@ void info() { DEBUG_MSG_P(PSTR("[MAIN] Board: %s\n"), getBoardName().c_str()); DEBUG_MSG_P(PSTR("[MAIN] Support: %s\n"), getEspurnaModules().c_str()); + DEBUG_MSG_P(PSTR("[MAIN] OTA: %s\n"), getEspurnaOTAModules().c_str()); #if SENSOR_SUPPORT DEBUG_MSG_P(PSTR("[MAIN] Sensors: %s\n"), getEspurnaSensors().c_str()); #endif // SENSOR_SUPPORT @@ -534,8 +518,6 @@ void info() { // SSL // ----------------------------------------------------------------------------- -#if ASYNC_TCP_SSL_ENABLED - bool sslCheckFingerPrint(const char * fingerprint) { return (strlen(fingerprint) == 59); } @@ -571,8 +553,6 @@ bool sslFingerPrintChar(const char * fingerprint, char * destination) { } -#endif - // ----------------------------------------------------------------------------- // Reset // ----------------------------------------------------------------------------- @@ -642,3 +622,19 @@ bool isNumber(const char * s) { } return digit; } + +// ref: lwip2 lwip_strnstr with strnlen +char* strnstr(const char* buffer, const char* token, size_t n) { + size_t token_len = strnlen(token, n); + if (token_len == 0) { + return const_cast(buffer); + } + + for (const char* p = buffer; *p && (p + token_len <= buffer + n); p++) { + if ((*p == *token) && (strncmp(p, token, token_len) == 0)) { + return const_cast(p); + } + } + + return nullptr; +} diff --git a/code/espurna/web.ino b/code/espurna/web.ino index 7c4b6f15..3c80fd2c 100644 --- a/code/espurna/web.ino +++ b/code/espurna/web.ino @@ -37,10 +37,10 @@ Copyright (C) 2016-2019 by Xose Pérez #endif // WEB_EMBEDDED -#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED +#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED #include "static/server.cer.h" #include "static/server.key.h" -#endif // ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED +#endif // SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED // ----------------------------------------------------------------------------- @@ -52,11 +52,19 @@ bool _webConfigSuccess = false; std::vector _web_request_callbacks; std::vector _web_body_callbacks; +constexpr const size_t WEB_CONFIG_BUFFER_MAX = 4096; + // ----------------------------------------------------------------------------- // HOOKS // ----------------------------------------------------------------------------- void _onReset(AsyncWebServerRequest *request) { + + webLog(request); + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } + deferredReset(100, CUSTOM_RESET_HTTP); request->send(200); } @@ -65,14 +73,17 @@ void _onDiscover(AsyncWebServerRequest *request) { webLog(request); - AsyncResponseStream *response = request->beginResponseStream("text/json"); + AsyncResponseStream *response = request->beginResponseStream("application/json"); + + const String device = getBoardName(); + const String hostname = getSetting("hostname"); - DynamicJsonBuffer jsonBuffer; + StaticJsonBuffer jsonBuffer; JsonObject &root = jsonBuffer.createObject(); root["app"] = APP_NAME; root["version"] = APP_VERSION; - root["hostname"] = getSetting("hostname"); - root["device"] = getBoardName(); + root["device"] = device.c_str(); + root["hostname"] = hostname.c_str(); root.printTo(*response); request->send(response); @@ -125,11 +136,13 @@ void _onPostConfig(AsyncWebServerRequest *request) { void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } + // No buffer if (final && (index == 0)) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.parseObject((char *) data); - if (root.success()) _webConfigSuccess = settingsRestoreJson(root); + _webConfigSuccess = settingsRestoreJson((char*) data); return; } @@ -144,6 +157,12 @@ void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t i // Copy if (len > 0) { + if ((_webConfigBuffer->size() + len) > std::min(WEB_CONFIG_BUFFER_MAX, getFreeHeap() - sizeof(std::vector))) { + delete _webConfigBuffer; + _webConfigBuffer = nullptr; + request->send(500); + return; + } _webConfigBuffer->reserve(_webConfigBuffer->size() + len); _webConfigBuffer->insert(_webConfigBuffer->end(), data, data + len); } @@ -152,11 +171,7 @@ void _onPostConfigData(AsyncWebServerRequest *request, String filename, size_t i if (final) { _webConfigBuffer->push_back(0); - - // Parse JSON - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.parseObject((char *) _webConfigBuffer->data()); - if (root.success()) _webConfigSuccess = settingsRestoreJson(root); + _webConfigSuccess = settingsRestoreJson((char*) _webConfigBuffer->data()); delete _webConfigBuffer; } @@ -177,7 +192,7 @@ void _onHome(AsyncWebServerRequest *request) { } else { - #if ASYNC_TCP_SSL_ENABLED + #if SECURE_CLIENT == SECURE_CLIENT_AXTLS // Chunked response, we calculate the chunks based on free heap (in multiples of 32) // This is necessary when a TLS connection is open since it sucks too much memory @@ -218,7 +233,7 @@ void _onHome(AsyncWebServerRequest *request) { } #endif -#if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED +#if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED int _onCertificate(void * arg, const char *filename, uint8_t **buf) { @@ -297,7 +312,11 @@ void _onUpgrade(AsyncWebServerRequest *request) { } -void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { +void _onUpgradeFile(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + + if (!webAuthenticate(request)) { + return request->requestAuthentication(getSetting("hostname").c_str()); + } if (!index) { @@ -331,32 +350,69 @@ void _onUpgradeData(AsyncWebServerRequest *request, String filename, size_t inde #endif } } else { - //Removed to avoid websocket ping back during upgrade (see #1574) - //DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), index + len); + // Removed to avoid websocket ping back during upgrade (see #1574) + // TODO: implement as separate from debugging message + if (wsConnected()) return; + DEBUG_MSG_P(PSTR("[UPGRADE] Progress: %u bytes\r"), index + len); + } +} + +bool _onAPModeRequest(AsyncWebServerRequest *request) { + + if ((WiFi.getMode() & WIFI_AP) > 0) { + const String domain = getSetting("hostname") + "."; + const String host = request->header("Host"); + const String ip = WiFi.softAPIP().toString(); + + // Only allow requests that use our hostname or ip + if (host.equals(ip)) return true; + if (host.startsWith(domain)) return true; + + // Immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660 + // Not doing so will cause memory exhaustion, because the connection will linger + request->send(404); + request->client()->close(); + + return false; } + + return true; + } void _onRequest(AsyncWebServerRequest *request){ + if (!_onAPModeRequest(request)) return; + // Send request to subscribers for (unsigned char i = 0; i < _web_request_callbacks.size(); i++) { bool response = (_web_request_callbacks[i])(request); if (response) return; } - // No subscriber handled the request, return a 404 + // No subscriber handled the request, return a 404 with implicit "Connection: close" request->send(404); + // And immediatly close the connection, ref: https://github.com/xoseperez/espurna/issues/1660 + // Not doing so will cause memory exhaustion, because the connection will linger + request->client()->close(); + } void _onBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { + if (!_onAPModeRequest(request)) return; + // Send request to subscribers for (unsigned char i = 0; i < _web_body_callbacks.size(); i++) { bool response = (_web_body_callbacks[i])(request, data, len, index, total); if (response) return; } + // Same as _onAPModeRequest(...) + request->send(404); + request->client()->close(); + } @@ -388,7 +444,7 @@ void webRequestRegister(web_request_callback_f callback) { } unsigned int webPort() { - #if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED + #if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED return 443; #else return getSetting("webPort", WEB_PORT).toInt(); @@ -420,7 +476,7 @@ void webSetup() { _server->on("/reset", HTTP_GET, _onReset); _server->on("/config", HTTP_GET, _onGetConfig); _server->on("/config", HTTP_POST | HTTP_PUT, _onPostConfig, _onPostConfigData); - _server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeData); + _server->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeFile); _server->on("/discover", HTTP_GET, _onDiscover); // Serve static files @@ -439,7 +495,7 @@ void webSetup() { _server->onNotFound(_onRequest); // Run server - #if ASYNC_TCP_SSL_ENABLED & WEB_SSL_ENABLED + #if SECURE_CLIENT == SECURE_CLIENT_AXTLS & WEB_SSL_ENABLED _server->onSslFileRequest(_onCertificate, NULL); _server->beginSecure("server.cer", "server.key", NULL); #else diff --git a/code/espurna/wifi.ino b/code/espurna/wifi.ino index 3d06fe6c..49ac7f75 100644 --- a/code/espurna/wifi.ino +++ b/code/espurna/wifi.ino @@ -6,40 +6,61 @@ Copyright (C) 2016-2019 by Xose Pérez */ -#include "JustWifi.h" +#include #include -uint32_t _wifi_scan_client_id = 0; bool _wifi_wps_running = false; bool _wifi_smartconfig_running = false; +bool _wifi_smartconfig_initial = false; uint8_t _wifi_ap_mode = WIFI_AP_FALLBACK; +#if WIFI_GRATUITOUS_ARP_SUPPORT +unsigned long _wifi_gratuitous_arp_interval = 0; +unsigned long _wifi_gratuitous_arp_last = 0; +#endif + // ----------------------------------------------------------------------------- // PRIVATE // ----------------------------------------------------------------------------- +struct wifi_scan_info_t { + String ssid_scan; + int32_t rssi_scan; + uint8_t sec_scan; + uint8_t* BSSID_scan; + int32_t chan_scan; + bool hidden_scan; + char buffer[128]; +}; + +void _wifiUpdateSoftAP() { + if (WiFi.softAPgetStationNum() == 0) { + #if USE_PASSWORD + jw.setSoftAP(getSetting("hostname").c_str(), getAdminPass().c_str()); + #else + jw.setSoftAP(getSetting("hostname").c_str()); + #endif + } +} void _wifiCheckAP() { - - if ((WIFI_AP_FALLBACK == _wifi_ap_mode) && - (jw.connected()) && - ((WiFi.getMode() & WIFI_AP) > 0) && - (WiFi.softAPgetStationNum() == 0) + if ( + (WIFI_AP_FALLBACK == _wifi_ap_mode) + && ((WiFi.getMode() & WIFI_AP) > 0) + && jw.connected() + && (WiFi.softAPgetStationNum() == 0) ) { - jw.enableAP(false); + jw.enableAP(false); } - } void _wifiConfigure() { jw.setHostname(getSetting("hostname").c_str()); - #if USE_PASSWORD - jw.setSoftAP(getSetting("hostname").c_str(), getAdminPass().c_str()); - #else - jw.setSoftAP(getSetting("hostname").c_str()); - #endif + _wifiUpdateSoftAP(); + jw.setConnectTimeout(WIFI_CONNECT_TIMEOUT); wifiReconnectCheck(); + jw.enableAPFallback(WIFI_FALLBACK_APMODE); jw.cleanNetworks(); @@ -55,94 +76,86 @@ void _wifiConfigure() { int i; for (i = 0; i< WIFI_MAX_NETWORKS; i++) { - if (getSetting("ssid" + String(i)).length() == 0) break; - if (getSetting("ip" + String(i)).length() == 0) { + if (!hasSetting("ssid", i)) break; + if (!hasSetting("ip", i)) { jw.addNetwork( - getSetting("ssid" + String(i)).c_str(), - getSetting("pass" + String(i)).c_str() + getSetting("ssid", i, "").c_str(), + getSetting("pass", i, "").c_str() ); } else { jw.addNetwork( - getSetting("ssid" + String(i)).c_str(), - getSetting("pass" + String(i)).c_str(), - getSetting("ip" + String(i)).c_str(), - getSetting("gw" + String(i)).c_str(), - getSetting("mask" + String(i)).c_str(), - getSetting("dns" + String(i)).c_str() + getSetting("ssid", i, "").c_str(), + getSetting("pass", i, "").c_str(), + getSetting("ip", i, "").c_str(), + getSetting("gw", i, "").c_str(), + getSetting("mask", i, "").c_str(), + getSetting("dns", i, "").c_str() ); } } + #if JUSTWIFI_ENABLE_SMARTCONFIG + if (i == 0) _wifi_smartconfig_initial = true; + #endif + jw.enableScan(getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1); unsigned char sleep_mode = getSetting("wifiSleep", WIFI_SLEEP_MODE).toInt(); sleep_mode = constrain(sleep_mode, 0, 2); WiFi.setSleepMode(static_cast(sleep_mode)); + + #if WIFI_GRATUITOUS_ARP_SUPPORT + _wifi_gratuitous_arp_last = millis(); + _wifi_gratuitous_arp_interval = getSetting("wifiGarpIntvl", secureRandom( + WIFI_GRATUITOUS_ARP_INTERVAL_MIN, WIFI_GRATUITOUS_ARP_INTERVAL_MAX + )).toInt(); + #endif + + const auto tx_power = getSetting("wifiTxPwr", WIFI_OUTPUT_POWER_DBM).toFloat(); + WiFi.setOutputPower(tx_power); + } -void _wifiScan(uint32_t client_id = 0) { +void _wifiScan(wifi_scan_f callback = nullptr) { DEBUG_MSG_P(PSTR("[WIFI] Start scanning\n")); - #if WEB_SUPPORT - String output; - #endif - unsigned char result = WiFi.scanNetworks(); if (result == WIFI_SCAN_FAILED) { DEBUG_MSG_P(PSTR("[WIFI] Scan failed\n")); - #if WEB_SUPPORT - output = String("Failed scan"); - #endif + return; } else if (result == 0) { DEBUG_MSG_P(PSTR("[WIFI] No networks found\n")); - #if WEB_SUPPORT - output = String("No networks found"); - #endif - } else { - - DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), result); - - // Populate defined networks with scan data - for (int8_t i = 0; i < result; ++i) { - - String ssid_scan; - int32_t rssi_scan; - uint8_t sec_scan; - uint8_t* BSSID_scan; - int32_t chan_scan; - bool hidden_scan; - char buffer[128]; - - WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan, hidden_scan); - - snprintf_P(buffer, sizeof(buffer), - PSTR("BSSID: %02X:%02X:%02X:%02X:%02X:%02X SEC: %s RSSI: %3d CH: %2d SSID: %s"), - BSSID_scan[0], BSSID_scan[1], BSSID_scan[2], BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], - (sec_scan != ENC_TYPE_NONE ? "YES" : "NO "), - rssi_scan, - chan_scan, - (char *) ssid_scan.c_str() - ); + return; + } - DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), buffer); + DEBUG_MSG_P(PSTR("[WIFI] %d networks found:\n"), result); - #if WEB_SUPPORT - if (client_id > 0) output = output + String(buffer) + String("
"); - #endif + // Populate defined networks with scan data + wifi_scan_info_t info; - } + for (unsigned char i = 0; i < result; ++i) { - } + WiFi.getNetworkInfo(i, info.ssid_scan, info.sec_scan, info.rssi_scan, info.BSSID_scan, info.chan_scan, info.hidden_scan); - #if WEB_SUPPORT - if (client_id > 0) { - output = String("{\"scanResult\": \"") + output + String("\"}"); - wsSend(client_id, output.c_str()); + snprintf_P(info.buffer, sizeof(info.buffer), + PSTR("BSSID: %02X:%02X:%02X:%02X:%02X:%02X SEC: %s RSSI: %3d CH: %2d SSID: %s"), + info.BSSID_scan[0], info.BSSID_scan[1], info.BSSID_scan[2], info.BSSID_scan[3], info.BSSID_scan[4], info.BSSID_scan[5], + (info.sec_scan != ENC_TYPE_NONE ? "YES" : "NO "), + info.rssi_scan, + info.chan_scan, + info.ssid_scan.c_str() + ); + + if (callback) { + callback(info); + } else { + DEBUG_MSG_P(PSTR("[WIFI] > %s\n"), info.buffer); } - #endif + + } WiFi.scanDelete(); @@ -195,25 +208,46 @@ void _wifiInject() { if (strlen(WIFI1_SSID)) { if (!hasSetting("ssid", 0)) { - setSetting("ssid", 0, WIFI1_SSID); - setSetting("pass", 0, WIFI1_PASS); - setSetting("ip", 0, WIFI1_IP); - setSetting("gw", 0, WIFI1_GW); - setSetting("mask", 0, WIFI1_MASK); - setSetting("dns", 0, WIFI1_DNS); + setSetting("ssid", 0, F(WIFI1_SSID)); + setSetting("pass", 0, F(WIFI1_PASS)); + setSetting("ip", 0, F(WIFI1_IP)); + setSetting("gw", 0, F(WIFI1_GW)); + setSetting("mask", 0, F(WIFI1_MASK)); + setSetting("dns", 0, F(WIFI1_DNS)); } if (strlen(WIFI2_SSID)) { if (!hasSetting("ssid", 1)) { - setSetting("ssid", 1, WIFI2_SSID); - setSetting("pass", 1, WIFI2_PASS); - setSetting("ip", 1, WIFI2_IP); - setSetting("gw", 1, WIFI2_GW); - setSetting("mask", 1, WIFI2_MASK); - setSetting("dns", 1, WIFI2_DNS); + setSetting("ssid", 1, F(WIFI2_SSID)); + setSetting("pass", 1, F(WIFI2_PASS)); + setSetting("ip", 1, F(WIFI2_IP)); + setSetting("gw", 1, F(WIFI2_GW)); + setSetting("mask", 1, F(WIFI2_MASK)); + setSetting("dns", 1, F(WIFI2_DNS)); } - } + if (strlen(WIFI3_SSID)) { + if (!hasSetting("ssid", 2)) { + setSetting("ssid", 2, F(WIFI3_SSID)); + setSetting("pass", 2, F(WIFI3_PASS)); + setSetting("ip", 2, F(WIFI3_IP)); + setSetting("gw", 2, F(WIFI3_GW)); + setSetting("mask", 2, F(WIFI3_MASK)); + setSetting("dns", 2, F(WIFI3_DNS)); + } + + if (strlen(WIFI4_SSID)) { + if (!hasSetting("ssid", 3)) { + setSetting("ssid", 3, F(WIFI4_SSID)); + setSetting("pass", 3, F(WIFI4_PASS)); + setSetting("ip", 3, F(WIFI4_IP)); + setSetting("gw", 3, F(WIFI4_GW)); + setSetting("mask", 3, F(WIFI4_MASK)); + setSetting("dns", 3, F(WIFI4_DNS)); + } + } + } + } } } @@ -230,7 +264,6 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) { if (MESSAGE_WPS_ERROR == code || MESSAGE_SMARTCONFIG_ERROR == code) { _wifi_wps_running = false; _wifi_smartconfig_running = false; - jw.enableAP(true); } if (MESSAGE_WPS_SUCCESS == code || MESSAGE_SMARTCONFIG_SUCCESS == code) { @@ -254,7 +287,6 @@ void _wifiCallback(justwifi_messages_t code, char * parameter) { _wifi_wps_running = false; _wifi_smartconfig_running = false; - jw.enableAP(true); } @@ -346,6 +378,7 @@ void _wifiDebugCallback(justwifi_messages_t code, char * parameter) { } if (code == MESSAGE_ACCESSPOINT_DESTROYED) { + _wifiUpdateSoftAP(); DEBUG_MSG_P(PSTR("[WIFI] Access point destroyed\n")); } @@ -400,6 +433,11 @@ void _wifiInitCommands() { terminalOK(); }); + terminalRegisterCommand(F("WIFI.STA"), [](Embedis* e) { + wifiStartSTA(); + terminalOK(); + }); + terminalRegisterCommand(F("WIFI.AP"), [](Embedis* e) { wifiStartAP(); terminalOK(); @@ -434,7 +472,7 @@ void _wifiInitCommands() { #if WEB_SUPPORT -bool _wifiWebSocketOnReceive(const char * key, JsonVariant& value) { +bool _wifiWebSocketOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "wifi", 4) == 0) return true; if (strncmp(key, "ssid", 4) == 0) return true; if (strncmp(key, "pass", 4) == 0) return true; @@ -445,7 +483,7 @@ bool _wifiWebSocketOnReceive(const char * key, JsonVariant& value) { return false; } -void _wifiWebSocketOnSend(JsonObject& root) { +void _wifiWebSocketOnConnected(JsonObject& root) { root["maxNetworks"] = WIFI_MAX_NETWORKS; root["wifiScan"] = getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1; JsonArray& wifi = root.createNestedArray("wifi"); @@ -461,16 +499,90 @@ void _wifiWebSocketOnSend(JsonObject& root) { } } +void _wifiWebSocketScan(JsonObject& root) { + JsonArray& scanResult = root.createNestedArray("scanResult"); + _wifiScan([&scanResult](wifi_scan_info_t& info) { + scanResult.add(info.buffer); + }); +} + void _wifiWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) { - if (strcmp(action, "scan") == 0) _wifi_scan_client_id = client_id; + if (strcmp(action, "scan") == 0) wsPost(client_id, _wifiWebSocketScan); } #endif +// ----------------------------------------------------------------------------- +// SUPPORT +// ----------------------------------------------------------------------------- + +#if WIFI_GRATUITOUS_ARP_SUPPORT + +// ref: lwip src/core/netif.c netif_issue_reports(...) +// ref: esp-lwip/core/ipv4/etharp.c garp_tmr() +// TODO: only for ipv4, need (?) a different method with ipv6 +bool _wifiSendGratuitousArp() { + + bool result = false; + for (netif* interface = netif_list; interface != nullptr; interface = interface->next) { + if ( + (interface->flags & NETIF_FLAG_ETHARP) + && (interface->hwaddr_len == ETHARP_HWADDR_LEN) + #if LWIP_VERSION_MAJOR == 1 + && (!ip_addr_isany(&interface->ip_addr)) + #else + && (!ip4_addr_isany_val(*netif_ip4_addr(interface))) + #endif + && (interface->flags & NETIF_FLAG_LINK_UP) + && (interface->flags & NETIF_FLAG_UP) + ) { + etharp_gratuitous(interface); + result = true; + } + } + + return result; +} + +void _wifiSendGratuitousArp(unsigned long interval) { + if (millis() - _wifi_gratuitous_arp_last > interval) { + _wifi_gratuitous_arp_last = millis(); + _wifiSendGratuitousArp(); + } +} + +#endif // WIFI_GRATUITOUS_ARP_SUPPORT + // ----------------------------------------------------------------------------- // INFO // ----------------------------------------------------------------------------- +// backported WiFiAPClass methods + +String _wifiSoftAPSSID() { + struct softap_config config; + wifi_softap_get_config(&config); + + char* name = reinterpret_cast(config.ssid); + char ssid[sizeof(config.ssid) + 1]; + memcpy(ssid, name, sizeof(config.ssid)); + ssid[sizeof(config.ssid)] = '\0'; + + return String(ssid); +} + +String _wifiSoftAPPSK() { + struct softap_config config; + wifi_softap_get_config(&config); + + char* pass = reinterpret_cast(config.password); + char psk[sizeof(config.password) + 1]; + memcpy(psk, pass, sizeof(config.password)); + psk[sizeof(config.password)] = '\0'; + + return String(psk); +} + void wifiDebug(WiFiMode_t modes) { #if DEBUG_SUPPORT @@ -478,7 +590,6 @@ void wifiDebug(WiFiMode_t modes) { if (((modes & WIFI_STA) > 0) && ((WiFi.getMode() & WIFI_STA) > 0)) { - uint8_t * bssid = WiFi.BSSID(); DEBUG_MSG_P(PSTR("[WIFI] ------------------------------------- MODE STA\n")); DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), WiFi.SSID().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.localIP().toString().c_str()); @@ -487,9 +598,7 @@ void wifiDebug(WiFiMode_t modes) { DEBUG_MSG_P(PSTR("[WIFI] DNS %s\n"), WiFi.dnsIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MASK %s\n"), WiFi.subnetMask().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] HOST http://%s.local\n"), WiFi.hostname().c_str()); - DEBUG_MSG_P(PSTR("[WIFI] BSSID %02X:%02X:%02X:%02X:%02X:%02X\n"), - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], bssid[6] - ); + DEBUG_MSG_P(PSTR("[WIFI] BSSID %s\n"), WiFi.BSSIDstr().c_str()); DEBUG_MSG_P(PSTR("[WIFI] CH %d\n"), WiFi.channel()); DEBUG_MSG_P(PSTR("[WIFI] RSSI %d\n"), WiFi.RSSI()); footer = true; @@ -498,8 +607,8 @@ void wifiDebug(WiFiMode_t modes) { if (((modes & WIFI_AP) > 0) && ((WiFi.getMode() & WIFI_AP) > 0)) { DEBUG_MSG_P(PSTR("[WIFI] -------------------------------------- MODE AP\n")); - DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), getSetting("hostname").c_str()); - DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), getAdminPass().c_str()); + DEBUG_MSG_P(PSTR("[WIFI] SSID %s\n"), _wifiSoftAPSSID().c_str()); + DEBUG_MSG_P(PSTR("[WIFI] PASS %s\n"), _wifiSoftAPPSK().c_str()); DEBUG_MSG_P(PSTR("[WIFI] IP %s\n"), WiFi.softAPIP().toString().c_str()); DEBUG_MSG_P(PSTR("[WIFI] MAC %s\n"), WiFi.softAPmacAddress().c_str()); footer = true; @@ -548,6 +657,12 @@ void wifiDisconnect() { jw.disconnect(); } +void wifiStartSTA() { + jw.disconnect(); + jw.enableSTA(true); + jw.enableAP(false); +} + void wifiStartAP(bool only) { if (only) { jw.enableSTA(false); @@ -610,6 +725,10 @@ void wifiSetup() { _wifiInject(); _wifiConfigure(); + #if JUSTWIFI_ENABLE_SMARTCONFIG + if (_wifi_smartconfig_initial) jw.startSmartConfig(); + #endif + // Message callbacks wifiRegister(_wifiCallback); #if WIFI_AP_CAPTIVE @@ -620,9 +739,10 @@ void wifiSetup() { #endif #if WEB_SUPPORT - wsOnSendRegister(_wifiWebSocketOnSend); - wsOnReceiveRegister(_wifiWebSocketOnReceive); - wsOnActionRegister(_wifiWebSocketOnAction); + wsRegister() + .onAction(_wifiWebSocketOnAction) + .onConnected(_wifiWebSocketOnConnected) + .onKeyCheck(_wifiWebSocketOnKeyCheck); #endif #if TERMINAL_SUPPORT @@ -647,11 +767,12 @@ void wifiLoop() { } #endif - // Do we have a pending scan? - if (_wifi_scan_client_id > 0) { - _wifiScan(_wifi_scan_client_id); - _wifi_scan_client_id = 0; - } + // Only send out gra arp when in STA mode + #if WIFI_GRATUITOUS_ARP_SUPPORT + if (_wifi_gratuitous_arp_interval) { + _wifiSendGratuitousArp(_wifi_gratuitous_arp_interval); + } + #endif // Check if we should disable AP static unsigned long last = 0; diff --git a/code/espurna/ws.ino b/code/espurna/ws.ino index 07a86774..132a596c 100644 --- a/code/espurna/ws.ino +++ b/code/espurna/ws.ino @@ -16,21 +16,146 @@ Copyright (C) 2016-2019 by Xose Pérez #include "libs/WebSocketIncommingBuffer.h" AsyncWebSocket _ws("/ws"); -Ticker _web_defer; +Ticker _ws_defer; +uint32_t _ws_last_update = 0; -std::vector _ws_on_send_callbacks; -std::vector _ws_on_action_callbacks; -std::vector _ws_on_receive_callbacks; +// ----------------------------------------------------------------------------- +// WS callbacks +// ----------------------------------------------------------------------------- + +ws_callbacks_t& ws_callbacks_t::onVisible(ws_on_send_callback_f cb) { + on_visible.push_back(cb); + return *this; +} + +ws_callbacks_t& ws_callbacks_t::onConnected(ws_on_send_callback_f cb) { + on_connected.push_back(cb); + return *this; +} + +ws_callbacks_t& ws_callbacks_t::onData(ws_on_send_callback_f cb) { + on_data.push_back(cb); + return *this; +} + +ws_callbacks_t& ws_callbacks_t::onAction(ws_on_action_callback_f cb) { + on_action.push_back(cb); + return *this; +} + +ws_callbacks_t& ws_callbacks_t::onKeyCheck(ws_on_keycheck_callback_f cb) { + on_keycheck.push_back(cb); + return *this; +} + +ws_callbacks_t _ws_callbacks; + +struct ws_counter_t { + + ws_counter_t() : current(0), start(0), stop(0) {} + + ws_counter_t(uint32_t start, uint32_t stop) : + current(start), start(start), stop(stop) {} + + void reset() { + current = start; + } + + void next() { + if (current < stop) { + ++current; + } + } + + bool done() { + return (current >= stop); + } + + uint32_t current; + uint32_t start; + uint32_t stop; +}; + +struct ws_data_t { + + enum mode_t { + SEQUENCE, + ALL + }; + + ws_data_t(const ws_on_send_callback_f& cb) : + storage(new ws_on_send_callback_list_t {cb}), + client_id(0), + mode(ALL), + callbacks(*storage.get()), + counter(0, 1) + {} + + ws_data_t(uint32_t client_id, const ws_on_send_callback_f& cb) : + storage(new ws_on_send_callback_list_t {cb}), + client_id(client_id), + mode(ALL), + callbacks(*storage.get()), + counter(0, 1) + {} + + ws_data_t(const uint32_t client_id, ws_on_send_callback_list_t&& callbacks, mode_t mode = SEQUENCE) : + storage(new ws_on_send_callback_list_t(std::move(callbacks))), + client_id(client_id), + mode(mode), + callbacks(*storage.get()), + counter(0, (storage.get())->size()) + {} + + ws_data_t(const uint32_t client_id, const ws_on_send_callback_list_t& callbacks, mode_t mode = SEQUENCE) : + client_id(client_id), + mode(mode), + callbacks(callbacks), + counter(0, callbacks.size()) + {} + + bool done() { + return counter.done(); + } + + void sendAll(JsonObject& root) { + while (!counter.done()) counter.next(); + for (auto& callback : callbacks) { + callback(root); + } + } + + void sendCurrent(JsonObject& root) { + callbacks[counter.current](root); + counter.next(); + } + + void send(JsonObject& root) { + switch (mode) { + case SEQUENCE: sendCurrent(root); break; + case ALL: sendAll(root); break; + } + } + + std::unique_ptr storage; + + const uint32_t client_id; + const mode_t mode; + const ws_on_send_callback_list_t& callbacks; + ws_counter_t counter; +}; + +std::queue _ws_client_data; // ----------------------------------------------------------------------------- -// Private methods +// WS authentication // ----------------------------------------------------------------------------- -typedef struct { +struct ws_ticket_t { IPAddress ip; unsigned long timestamp = 0; -} ws_ticket_t; -ws_ticket_t _ticket[WS_BUFFER_SIZE]; +}; +ws_ticket_t _ws_tickets[WS_BUFFER_SIZE]; void _onAuth(AsyncWebServerRequest *request) { @@ -41,15 +166,15 @@ void _onAuth(AsyncWebServerRequest *request) { unsigned long now = millis(); unsigned short index; for (index = 0; index < WS_BUFFER_SIZE; index++) { - if (_ticket[index].ip == ip) break; - if (_ticket[index].timestamp == 0) break; - if (now - _ticket[index].timestamp > WS_TIMEOUT) break; + if (_ws_tickets[index].ip == ip) break; + if (_ws_tickets[index].timestamp == 0) break; + if (now - _ws_tickets[index].timestamp > WS_TIMEOUT) break; } if (index == WS_BUFFER_SIZE) { request->send(429); } else { - _ticket[index].ip = ip; - _ticket[index].timestamp = now; + _ws_tickets[index].ip = ip; + _ws_tickets[index].timestamp = now; request->send(200, "text/plain", "OK"); } @@ -62,7 +187,7 @@ bool _wsAuth(AsyncWebSocketClient * client) { unsigned short index = 0; for (index = 0; index < WS_BUFFER_SIZE; index++) { - if ((_ticket[index].ip == ip) && (now - _ticket[index].timestamp < WS_TIMEOUT)) break; + if ((_ws_tickets[index].ip == ip) && (now - _ws_tickets[index].timestamp < WS_TIMEOUT)) break; } if (index == WS_BUFFER_SIZE) { @@ -73,39 +198,97 @@ bool _wsAuth(AsyncWebSocketClient * client) { } +// ----------------------------------------------------------------------------- +// Debug +// ----------------------------------------------------------------------------- + #if DEBUG_WEB_SUPPORT -bool wsDebugSend(const char* prefix, const char* message) { - if (!wsConnected()) return false; - if (getFreeHeap() < (strlen(message) * 3)) return false; +struct ws_debug_msg_t { + ws_debug_msg_t(const char* prefix, const char* message) : + prefix(prefix), message(message) + {} + String prefix; + String message; +}; + +struct ws_debug_t { + + ws_debug_t(size_t capacity) : + flush(false), + current(0), + capacity(capacity) + { + messages.reserve(capacity); + } - DynamicJsonBuffer jsonBuffer; - JsonObject &root = jsonBuffer.createObject(); - JsonObject &weblog = root.createNestedObject("weblog"); + void clear() { + messages.clear(); + current = 0; + flush = false; + } - weblog.set("message", message); - if (prefix && (prefix[0] != '\0')) { - weblog.set("prefix", prefix); + void add(const char* prefix, const char* message) { + if (current >= capacity) { + flush = true; + send(wsConnected()); + } + + messages.emplace(messages.begin() + current, prefix, message); + flush = true; + ++current; } - wsSend(root); + void send(const bool connected) { + if (!connected && flush) { + clear(); + return; + } - return true; -} -#endif + if (!flush) return; + // ref: http://arduinojson.org/v5/assistant/ + // {"weblog": {"msg":[...],"pre":[...]}} + DynamicJsonBuffer jsonBuffer(2*JSON_ARRAY_SIZE(messages.size()) + JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(2)); -// ----------------------------------------------------------------------------- + JsonObject& root = jsonBuffer.createObject(); + JsonObject& weblog = root.createNestedObject("weblog"); + + JsonArray& msg = weblog.createNestedArray("msg"); + JsonArray& pre = weblog.createNestedArray("pre"); -#if MQTT_SUPPORT -void _wsMQTTCallback(unsigned int type, const char * topic, const char * payload) { - if (type == MQTT_CONNECT_EVENT) wsSend_P(PSTR("{\"mqttStatus\": true}")); - if (type == MQTT_DISCONNECT_EVENT) wsSend_P(PSTR("{\"mqttStatus\": false}")); + for (auto& message : messages) { + pre.add(message.prefix.c_str()); + msg.add(message.message.c_str()); + } + + wsSend(root); + clear(); + } + + bool flush; + size_t current; + const size_t capacity; + std::vector messages; + +}; + +// TODO: move to the headers? +constexpr const size_t WS_DEBUG_MSG_BUFFER = 8; +ws_debug_t _ws_debug(WS_DEBUG_MSG_BUFFER); + +bool wsDebugSend(const char* prefix, const char* message) { + if (!wsConnected()) return false; + _ws_debug.add(prefix, message); + return true; } + #endif -bool _wsStore(String key, String value) { +// Check the existing setting before saving it +// TODO: this should know of the default values, somehow? +// TODO: move webPort handling somewhere else? +bool _wsStore(const String& key, const String& value) { - // HTTP port if (key == "webPort") { if ((value.toInt() == 0) || (value.toInt() == 80)) { return delSetting(key); @@ -120,7 +303,11 @@ bool _wsStore(String key, String value) { } -bool _wsStore(String key, JsonArray& value) { +// ----------------------------------------------------------------------------- +// Store indexed key (key0, key1, etc.) from array +// ----------------------------------------------------------------------------- + +bool _wsStore(const String& key, JsonArray& value) { bool changed = false; @@ -140,6 +327,15 @@ bool _wsStore(String key, JsonArray& value) { } +bool _wsCheckKey(const String& key, JsonVariant& value) { + for (auto& callback : _ws_callbacks.on_keycheck) { + if (callback(key.c_str(), value)) return true; + // TODO: remove this to call all OnKeyCheckCallbacks with the + // current key/value + } + return false; +} + void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { //DEBUG_MSG_P(PSTR("[WEBSOCKET] Parsing: %s\n"), length ? (char*) payload : ""); @@ -147,11 +343,22 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { // Get client ID uint32_t client_id = client->id(); + // Check early for empty object / nothing + if ((length == 0) || (length == 1)) { + return; + } + + if ((length == 3) && (strcmp((char*) payload, "{}") == 0)) { + return; + } + // Parse JSON input - DynamicJsonBuffer jsonBuffer; + // TODO: json buffer should be pretty efficient with the non-const payload, + // most of the space is taken by the object key references + DynamicJsonBuffer jsonBuffer(512); JsonObject& root = jsonBuffer.parseObject((char *) payload); if (!root.success()) { - DEBUG_MSG_P(PSTR("[WEBSOCKET] Error parsing data\n")); + DEBUG_MSG_P(PSTR("[WEBSOCKET] JSON parsing error\n")); wsSend_P(client_id, PSTR("{\"message\": 3}")); return; } @@ -169,7 +376,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { } if (strcmp(action, "reconnect") == 0) { - _web_defer.once_ms(100, wifiDisconnect); + _ws_defer.once_ms(100, wifiDisconnect); return; } @@ -184,8 +391,8 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { if (data.success()) { // Callbacks - for (unsigned char i = 0; i < _ws_on_action_callbacks.size(); i++) { - (_ws_on_action_callbacks[i])(client_id, action, data); + for (auto& callback : _ws_callbacks.on_action) { + callback(client_id, action, data); } // Restore configuration via websockets @@ -212,9 +419,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { String adminPass; bool save = false; - #if MQTT_SUPPORT - bool changedMQTT = false; - #endif for (auto kv: config) { @@ -240,15 +444,7 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { continue; } - // Check if key has to be processed - bool found = false; - for (unsigned char i = 0; i < _ws_on_receive_callbacks.size(); i++) { - found |= (_ws_on_receive_callbacks[i])(key.c_str(), value); - // TODO: remove this to call all OnReceiveCallbacks with the - // current key/value - if (found) break; - } - if (!found) { + if (!_wsCheckKey(key, value)) { delSetting(key); continue; } @@ -263,9 +459,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { // Update flags if value has changed if (changed) { save = true; - #if MQTT_SUPPORT - if (key.startsWith("mqtt")) changedMQTT = true; - #endif } } @@ -276,12 +469,6 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) { // Callbacks espurnaReload(); - // This should got to callback as well - // but first change management has to be in place - #if MQTT_SUPPORT - if (changedMQTT) mqttReset(); - #endif - // Persist settings saveSettings(); @@ -310,21 +497,19 @@ void _wsUpdate(JsonObject& root) { #endif } -void _wsDoUpdate(bool reset = false) { - static unsigned long last = millis(); - if (reset) { - last = millis() + WS_UPDATE_INTERVAL; - return; - } +void _wsResetUpdateTimer() { + _ws_last_update = millis() + WS_UPDATE_INTERVAL; +} - if (millis() - last > WS_UPDATE_INTERVAL) { - last = millis(); +void _wsDoUpdate(const bool connected) { + if (!connected) return; + if (millis() - _ws_last_update > WS_UPDATE_INTERVAL) { + _ws_last_update = millis(); wsSend(_wsUpdate); } } - -bool _wsOnReceive(const char * key, JsonVariant& value) { +bool _wsOnKeyCheck(const char * key, JsonVariant& value) { if (strncmp(key, "ws", 2) == 0) return true; if (strncmp(key, "admin", 5) == 0) return true; if (strncmp(key, "hostname", 8) == 0) return true; @@ -333,15 +518,9 @@ bool _wsOnReceive(const char * key, JsonVariant& value) { return false; } -void _wsOnStart(JsonObject& root) { +void _wsOnConnected(JsonObject& root) { char chipid[7]; snprintf_P(chipid, sizeof(chipid), PSTR("%06X"), ESP.getChipId()); - uint8_t * bssid = WiFi.BSSID(); - char bssid_str[20]; - snprintf_P(bssid_str, sizeof(bssid_str), - PSTR("%02X:%02X:%02X:%02X:%02X:%02X"), - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5] - ); root["webMode"] = WEB_MODE_NORMAL; @@ -354,7 +533,7 @@ void _wsOnStart(JsonObject& root) { root["manufacturer"] = MANUFACTURER; root["chipid"] = String(chipid); root["mac"] = WiFi.macAddress(); - root["bssid"] = String(bssid_str); + root["bssid"] = WiFi.BSSIDstr(); root["channel"] = WiFi.channel(); root["device"] = DEVICE; root["hostname"] = getSetting("hostname"); @@ -369,17 +548,12 @@ void _wsOnStart(JsonObject& root) { root["btnDelay"] = getSetting("btnDelay", BUTTON_DBLCLICK_DELAY).toInt(); root["webPort"] = getSetting("webPort", WEB_PORT).toInt(); root["wsAuth"] = getSetting("wsAuth", WS_AUTHENTICATION).toInt() == 1; - #if TERMINAL_SUPPORT - root["cmdVisible"] = 1; - #endif root["hbMode"] = getSetting("hbMode", HEARTBEAT_MODE).toInt(); root["hbInterval"] = getSetting("hbInterval", HEARTBEAT_INTERVAL).toInt(); - - _wsDoUpdate(true); - } void wsSend(JsonObject& root) { + // TODO: avoid serializing twice? size_t len = root.measureLength(); AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); @@ -393,6 +567,7 @@ void wsSend(uint32_t client_id, JsonObject& root) { AsyncWebSocketClient* client = _ws.client(client_id); if (client == nullptr) return; + // TODO: avoid serializing twice? size_t len = root.measureLength(); AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); @@ -402,29 +577,24 @@ void wsSend(uint32_t client_id, JsonObject& root) { } } -void _wsStart(uint32_t client_id) { +void _wsConnected(uint32_t client_id) { - #if USE_PASSWORD && WEB_FORCE_PASS_CHANGE - bool changePassword = getAdminPass().equals(ADMIN_PASS); - #else - bool changePassword = false; - #endif + const bool changePassword = (USE_PASSWORD && WEB_FORCE_PASS_CHANGE) + ? getAdminPass().equals(ADMIN_PASS) + : false; if (changePassword) { - DynamicJsonBuffer jsonBuffer; + StaticJsonBuffer jsonBuffer; JsonObject& root = jsonBuffer.createObject(); root["webMode"] = WEB_MODE_PASSWORD; wsSend(client_id, root); return; } - for (auto& callback : _ws_on_send_callbacks) { - DynamicJsonBuffer jsonBuffer; - JsonObject& root = jsonBuffer.createObject(); - callback(root); - wsSend(client_id, root); - } - + 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){ @@ -444,9 +614,10 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy IPAddress ip = client->remoteIP(); DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u connected, ip: %d.%d.%d.%d, url: %s\n"), client->id(), ip[0], ip[1], ip[2], ip[3], server->url()); - _wsStart(client->id()); - client->_tempObject = new WebSocketIncommingBuffer(&_wsParse, true); + _wsConnected(client->id()); + _wsResetUpdateTimer(); wifiReconnectCheck(); + client->_tempObject = new WebSocketIncommingBuffer(_wsParse, true); } else if(type == WS_EVT_DISCONNECT) { DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u disconnected\n"), client->id()); @@ -463,6 +634,7 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy } else if(type == WS_EVT_DATA) { //DEBUG_MSG_P(PSTR("[WEBSOCKET] #%u data(%u): %s\n"), client->id(), len, len ? (char*) data : ""); + if (!client->_tempObject) return; WebSocketIncommingBuffer *buffer = (WebSocketIncommingBuffer *)client->_tempObject; AwsFrameInfo * info = (AwsFrameInfo*)arg; buffer->data_event(client, info, data, len); @@ -471,9 +643,62 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy } +// TODO: make this generic loop method to queue important ws messages? +// or, if something uses ticker / async ctx to send messages, +// it needs a retry mechanism built into the callback object +void _wsHandleClientData(const bool connected) { + + if (!connected && !_ws_client_data.empty()) { + _ws_client_data.pop(); + return; + } + + if (_ws_client_data.empty()) return; + auto& data = _ws_client_data.front(); + + // client_id == 0 means we need to send the message to every client + if (data.client_id) { + AsyncWebSocketClient* ws_client = _ws.client(data.client_id); + + if (!ws_client) { + _ws_client_data.pop(); + return; + } + + // wait until we can send the next batch of messages + // XXX: enforce that callbacks send only one message per iteration + if (ws_client->queueIsFull()) { + return; + } + } + + // XXX: block allocation will try to create *2 next time, + // likely failing and causing wsSend to reference empty objects + // XXX: arduinojson6 will not do this, but we may need to use per-callback buffers + constexpr const size_t BUFFER_SIZE = 3192; + DynamicJsonBuffer jsonBuffer(BUFFER_SIZE); + JsonObject& root = jsonBuffer.createObject(); + + data.send(root); + if (data.client_id) { + wsSend(data.client_id, root); + } else { + wsSend(root); + } + yield(); + + if (data.done()) { + _ws_client_data.pop(); + } +} + void _wsLoop() { - if (!wsConnected()) return; - _wsDoUpdate(); + const bool connected = wsConnected(); + _wsDoUpdate(connected); + _wsHandleClientData(connected); + #if DEBUG_WEB_SUPPORT + _ws_debug.send(connected); + #endif } // ----------------------------------------------------------------------------- @@ -488,21 +713,13 @@ bool wsConnected(uint32_t client_id) { return _ws.hasClient(client_id); } -void wsOnSendRegister(ws_on_send_callback_f callback) { - _ws_on_send_callbacks.push_back(callback); -} - -void wsOnReceiveRegister(ws_on_receive_callback_f callback) { - _ws_on_receive_callbacks.push_back(callback); -} - -void wsOnActionRegister(ws_on_action_callback_f callback) { - _ws_on_action_callbacks.push_back(callback); +ws_callbacks_t& wsRegister() { + return _ws_callbacks; } void wsSend(ws_on_send_callback_f callback) { if (_ws.count() > 0) { - DynamicJsonBuffer jsonBuffer; + DynamicJsonBuffer jsonBuffer(512); JsonObject& root = jsonBuffer.createObject(); callback(root); @@ -528,17 +745,10 @@ void wsSend(uint32_t client_id, ws_on_send_callback_f callback) { AsyncWebSocketClient* client = _ws.client(client_id); if (client == nullptr) return; - DynamicJsonBuffer jsonBuffer; + DynamicJsonBuffer jsonBuffer(512); JsonObject& root = jsonBuffer.createObject(); callback(root); - - size_t len = root.measureLength(); - AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); - - if (buffer) { - root.printTo(reinterpret_cast(buffer->get()), len + 1); - client->text(buffer); - } + wsSend(client_id, root); } void wsSend(uint32_t client_id, const char * payload) { @@ -551,25 +761,52 @@ void wsSend_P(uint32_t client_id, PGM_P payload) { _ws.text(client_id, buffer); } +void wsPost(const ws_on_send_callback_f& cb) { + _ws_client_data.emplace(cb); +} + +void wsPost(uint32_t client_id, const ws_on_send_callback_f& cb) { + _ws_client_data.emplace(client_id, cb); +} + +void wsPostAll(uint32_t client_id, const ws_on_send_callback_list_t& cbs) { + _ws_client_data.emplace(client_id, cbs, ws_data_t::ALL); +} + +void wsPostAll(const ws_on_send_callback_list_t& cbs) { + _ws_client_data.emplace(0, cbs, ws_data_t::ALL); +} + +void wsPostSequence(uint32_t client_id, const ws_on_send_callback_list_t& cbs) { + _ws_client_data.emplace(client_id, cbs, ws_data_t::SEQUENCE); +} + +void wsPostSequence(uint32_t client_id, ws_on_send_callback_list_t&& cbs) { + _ws_client_data.emplace(client_id, std::forward(cbs), ws_data_t::SEQUENCE); +} + +void wsPostSequence(const ws_on_send_callback_list_t& cbs) { + _ws_client_data.emplace(0, cbs, ws_data_t::SEQUENCE); +} + void wsSetup() { _ws.onEvent(_wsEvent); webServer()->addHandler(&_ws); // CORS - #ifdef WEB_REMOTE_DOMAIN - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", WEB_REMOTE_DOMAIN); + const String webDomain = getSetting("webDomain", WEB_REMOTE_DOMAIN); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", webDomain); + if (!webDomain.equals("*")) { DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true"); - #endif + } webServer()->on("/auth", HTTP_GET, _onAuth); - #if MQTT_SUPPORT - mqttRegister(_wsMQTTCallback); - #endif + wsRegister() + .onConnected(_wsOnConnected) + .onKeyCheck(_wsOnKeyCheck); - wsOnSendRegister(_wsOnStart); - wsOnReceiveRegister(_wsOnReceive); espurnaRegisterLoop(_wsLoop); } diff --git a/code/extra_script_pre.py b/code/extra_script_pre.py new file mode 100644 index 00000000..04818885 --- /dev/null +++ b/code/extra_script_pre.py @@ -0,0 +1,90 @@ +from __future__ import print_function + +Import("env") + +import os +import sys + + +TRAVIS = os.environ.get("TRAVIS") +PIO_PLATFORM = env.PioPlatform() +CONFIG = env.GetProjectConfig() + + +class ExtraScriptError(Exception): + pass + + +# Most portable way, without depending on platformio internals +def subprocess_libdeps(lib_deps, storage=None, silent=True): + import subprocess + + args = [env.subst("$PYTHONEXE"), "-mplatformio", "lib"] + if not storage: + args.append("-g") + else: + args.extend(["-d", storage]) + args.append("install") + if silent: + args.append("-s") + + args.extend(lib_deps) + + subprocess.check_call(args) + + +# Avoid spawning pio lib every time, hook into the LibraryManager API (sort-of internal) +def library_manager_libdeps(lib_deps, storage=None): + from platformio.managers.lib import LibraryManager + from platformio.project.helpers import get_project_global_lib_dir + + if not storage: + manager = LibraryManager(get_project_global_lib_dir()) + else: + manager = LibraryManager(storage) + + for lib in lib_deps: + if manager.get_package_dir(*manager.parse_pkg_uri(lib)): + continue + print("installing: {}".format(lib), file=sys.stderr) + manager.install(lib) + + +def get_shared_libdeps_dir(section, name): + + if not CONFIG.has_option(section, name): + raise ExtraScriptError("{}.{} is required to be set".format(section, name)) + + opt = CONFIG.get(section, name) + + if not opt in env.GetProjectOption("lib_extra_dirs"): + raise ExtraScriptError( + "lib_extra_dirs must contain {}.{}".format(section, name) + ) + + return os.path.join(env["PROJECT_DIR"], opt) + + +def ensure_platform_updated(): + try: + if PIO_PLATFORM.are_outdated_packages(): + print("updating platform packages", file=sys.stderr) + PIO_PLATFORM.update_packages() + except Exception: + print("Warning: no connection, cannot check for outdated packages", file=sys.stderr) + +# latest toolchain is still optional with PIO (TODO: recheck after 2.6.0!) +# also updates arduino core git to the latest master commit +if TRAVIS and (env.GetProjectOption("platform") == CONFIG.get("common", "arduino_core_git")): + ensure_platform_updated() + +# to speed-up build process, install libraries in either global or local shared storage +if os.environ.get("ESPURNA_PIO_SHARED_LIBRARIES"): + if TRAVIS: + storage = None + print("using global library storage", file=sys.stderr) + else: + storage = get_shared_libdeps_dir("common", "shared_libdeps_dir") + print("using shared library storage: ", storage, file=sys.stderr) + + subprocess_libdeps(env.GetProjectOption("lib_deps"), storage) diff --git a/code/extra_scripts.py b/code/extra_scripts.py index 536f112b..eaca0e7a 100644 --- a/code/extra_scripts.py +++ b/code/extra_scripts.py @@ -8,6 +8,9 @@ import click Import("env", "projenv") +PIO_PLATFORM = env.PioPlatform() +FRAMEWORK_DIR = PIO_PLATFORM.get_package_dir("framework-arduinoespressif8266") + # ------------------------------------------------------------------------------ # Utils # ------------------------------------------------------------------------------ @@ -44,6 +47,31 @@ def print_filler(fill, color=Color.WHITE, err=False): out = sys.stderr if err else sys.stdout print(clr(color, fill * width), file=out) +def ldscript_inject_libpath(): + + # espressif8266@1.5.0 did not append this directory into the LIBPATH + libpath_sdk = os.path.join(FRAMEWORK_DIR, "tools", "sdk", "ld") + env.Append(LIBPATH=[libpath_sdk]) + + libpath_base = os.path.join("$PROJECT_DIR", "..", "dist", "ld") + env.Append(LIBPATH=[ + os.path.join(libpath_base, "pre_2.5.0") + ]) + + # local.eagle.app.v6.common.ld exists only with Core >2.5.0 + def check_local_ld(target ,source, env): + local_ld = env.subst(os.path.join("$BUILD_DIR", "ld", "local.eagle.app.v6.common.ld")) + if os.path.exists(local_ld): + env.Prepend(LIBPATH=[ + os.path.join(libpath_base, "latest") + ]) + + env.AddPreAction( + os.path.join("$BUILD_DIR", "firmware.elf"), + check_local_ld + ) + + # ------------------------------------------------------------------------------ # Callbacks # ------------------------------------------------------------------------------ @@ -97,19 +125,16 @@ def patch_lwip(): if "lwip_gcc" not in env["LIBS"]: return - framework_dir = env["FRAMEWORK_ARDUINOESP8266_DIR"] - - platform = env.PioPlatform() - toolchain_prefix = os.path.join(platform.get_package_dir("toolchain-xtensa"), "bin", "xtensa-lx106-elf-") + toolchain_prefix = os.path.join(PIO_PLATFORM.get_package_dir("toolchain-xtensa"), "bin", "xtensa-lx106-elf-") patch_action = env.VerboseAction(" ".join([ "-patch", "-u", "-N", "-d", - os.path.join(framework_dir, "tools", "sdk", "lwip"), + os.path.join(FRAMEWORK_DIR, "tools", "sdk", "lwip"), os.path.join("src", "core", "tcp_out.c"), env.subst(os.path.join("$PROJECT_DIR", "..", "dist", "patches", "lwip_mtu_issue_1610.patch")) ]), "Patching lwip source") build_action = env.VerboseAction(" ".join([ - "make", "-C", os.path.join(framework_dir, "tools", "sdk", "lwip", "src"), + "make", "-C", os.path.join(FRAMEWORK_DIR, "tools", "sdk", "lwip", "src"), "install", "TOOLS_PATH={}".format(toolchain_prefix), "LWIP_LIB=liblwip_gcc.a" @@ -131,6 +156,7 @@ projenv.ProcessUnFlags("-w") # 2.4.0 and up remove_float_support() +ldscript_inject_libpath() # two-step update hint when using 1MB boards env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_size) diff --git a/code/html/custom.js b/code/html/custom.js index ef831b14..49bac237 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -12,6 +12,7 @@ var numChanged = 0; var numReboot = 0; var numReconnect = 0; var numReload = 0; +var conf_saved = false; var useWhite = false; var useCCT = false; @@ -24,6 +25,10 @@ var packets; var filters = []; + +var magnitudes = []; + + // ----------------------------------------------------------------------------- // Messages // ----------------------------------------------------------------------------- @@ -374,7 +379,7 @@ function checkTempRangeMin() { $("#tempRangeMinInput").val(max - 1); } } - + function checkTempRangeMax() { var min = parseInt($("#tempRangeMinInput").val(), 10); var max = parseInt($("#tempRangeMaxInput").val(), 10); @@ -455,6 +460,7 @@ function setOriginalsFromValues(force) { function resetOriginals() { setOriginalsFromValues(true); numReboot = numReconnect = numReload = 0; + conf_saved = false; } function doReload(milliseconds) { @@ -514,47 +520,35 @@ function doUpgrade() { var data = new FormData(); data.append("upgrade", file, file.name); - $.ajax({ + var xhr = new XMLHttpRequest(); - // Your server script to process the upload - url: urls.upgrade.href, - type: "POST", - - // Form data - data: data, - - // Tell jQuery not to process data or worry about content-type - // You *must* include these options! - cache: false, - contentType: false, - processData: false, + var network_error = function() { + alert("There was a network error trying to upload the new image, please try again."); + }; + xhr.addEventListener("error", network_error, false); + xhr.addEventListener("abort", network_error, false); + + xhr.addEventListener("load", function(e) { + $("#upgrade-progress").hide(); + if ("OK" === xhr.responseText) { + alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds."); + doReload(5000); + } else { + alert("There was an error trying to upload the new image, please try again (" + + "response: " + xhr.responseText + ", " + + "status: " + xhr.statusText + ")"); + } + }, false); - success: function(data, text) { - $("#upgrade-progress").hide(); - if ("OK" === data) { - alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds."); - doReload(5000); - } else { - alert("There was an error trying to upload the new image, please try again (" + data + ")."); - } - }, - - // Custom XMLHttpRequest - xhr: function() { - $("#upgrade-progress").show(); - var myXhr = $.ajaxSettings.xhr(); - if (myXhr.upload) { - // For handling the progress of the upload - myXhr.upload.addEventListener("progress", function(e) { - if (e.lengthComputable) { - $("progress").attr({ value: e.loaded, max: e.total }); - } - } , false); - } - return myXhr; + xhr.upload.addEventListener("progress", function(e) { + $("#upgrade-progress").show(); + if (e.lengthComputable) { + $("progress").attr({ value: e.loaded, max: e.total }); } + }, false); - }); + xhr.open("POST", urls.upgrade.href); + xhr.send(data); }); @@ -616,6 +610,31 @@ function doReconnect(ask) { } +function doCheckOriginals() { + var response; + + if (numReboot > 0) { + response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?"); + if (response) { doReboot(false); } + } else if (numReconnect > 0) { + response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?"); + if (response) { doReconnect(false); } + } else if (numReload > 0) { + response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?"); + if (response) { doReload(0); } + } + + resetOriginals(); +} + +function waitForSave(){ + if (conf_saved == false) { + setTimeout(waitForSave, 1000); + } else { + doCheckOriginals(); + } +} + function doUpdate() { var forms = $(".form-settings"); @@ -632,24 +651,8 @@ function doUpdate() { // Change handling numChanged = 0; - setTimeout(function() { - - var response; - - if (numReboot > 0) { - response = window.confirm("You have to reboot the board for the changes to take effect, do you want to do it now?"); - if (response) { doReboot(false); } - } else if (numReconnect > 0) { - response = window.confirm("You have to reconnect to the WiFi for the changes to take effect, do you want to do it now?"); - if (response) { doReconnect(false); } - } else if (numReload > 0) { - response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?"); - if (response) { doReload(0); } - } - resetOriginals(); - - }, 1000); + waitForSave(); } @@ -846,7 +849,7 @@ function createMagnitudeList(data, container, template_name) { for (var i=0; i -function initColor(rgb) { +// wheelColorPicker accepts: +// hsv(0...360,0...1,0...1) +// hsv(0...100%,0...100%,0...100%) +// While we use: +// hsv(0...360,0...100%,0...100%) + +function _hsv_round(value) { + return Math.round(value * 100) / 100; +} + +function getPickerRGB(picker) { + return $(picker).wheelColorPicker("getValue", "css"); +} + +function setPickerRGB(picker, color) { + $(picker).wheelColorPicker("setValue", value, true); +} + +// TODO: use pct values instead of doing conversion? +function getPickerHSV(picker) { + var color = $(picker).wheelColorPicker("getColor"); + return String(Math.ceil(_hsv_round(color.h) * 360)) + + "," + String(Math.ceil(_hsv_round(color.s) * 100)) + + "," + String(Math.ceil(_hsv_round(color.v) * 100)); +} + +function setPickerHSV(picker, value) { + if (value === getPickerHSV(picker)) return; + var chunks = value.split(","); + $(picker).wheelColorPicker("setColor", { + h: _hsv_round(chunks[0] / 360), + s: _hsv_round(chunks[1] / 100), + v: _hsv_round(chunks[2] / 100) + }); +} + +function initColor(cfg) { + var rgb = false; + if (typeof cfg === "object") { + rgb = cfg.rgb; + } // check if already initialized var done = $("#colors > div").length; @@ -1127,15 +1186,12 @@ function initColor(rgb) { // init color wheel $("input[name='color']").wheelColorPicker({ - sliders: (rgb ? "wrgbp" : "whsvp") + sliders: (rgb ? "wrgbp" : "whsp") }).on("sliderup", function() { if (rgb) { - var value = $(this).wheelColorPicker("getValue", "css"); - sendAction("color", {rgb: value}); + sendAction("color", {rgb: getPickerRGB(this)}); } else { - var color = $(this).wheelColorPicker("getColor"); - var value = parseInt(color.h * 360, 10) + "," + parseInt(color.s * 100, 10) + "," + parseInt(color.v * 100, 10); - sendAction("color", {hsv: value}); + sendAction("color", {hsv: getPickerHSV(this)}); } }); @@ -1493,20 +1549,14 @@ function processData(data) { if ("rgb" === key) { - initColor(true); - $("input[name='color']").wheelColorPicker("setValue", value, true); + initColor({rgb: true}); + setPickerRGB($("input[name='color']"), value); return; } if ("hsv" === key) { - initColor(false); - // wheelColorPicker expects HSV to be between 0 and 1 all of them - var chunks = value.split(","); - var obj = {}; - obj.h = chunks[0] / 360; - obj.s = chunks[1] / 100; - obj.v = chunks[2] / 100; - $("input[name='color']").wheelColorPicker("setColor", obj); + initColor({hsv: true}); + setPickerHSV($("input[name='color']"), value); return; } @@ -1550,12 +1600,15 @@ function processData(data) { - if ("magnitudes" === key) { + if ("magnitudesConfig" === key) { initMagnitudes(value); + } + + if ("magnitudes" === key) { for (var i=0; i 0 || numReload > 0 || numReconnect > 0)){ + conf_saved = true; + } window.alert(messages[value]); return; } @@ -1707,10 +1761,15 @@ function processData(data) { if ("weblog" === key) { send("{}"); - if (value.prefix) { - $("#weblog").append(new Text(value.prefix)); + msg = value["msg"]; + pre = value["pre"]; + + for (var i=0; i < msg.length; ++i) { + if (pre[i]) { + $("#weblog").append(new Text(pre[i])); + } + $("#weblog").append(new Text(msg[i])); } - $("#weblog").append(new Text(value.message)); $("#weblog").scrollTop($("#weblog")[0].scrollHeight - $("#weblog").height()); return; @@ -1785,9 +1844,16 @@ function processData(data) { // Look for SPANs var span = $("span[name='" + key + "']"); if (span.length > 0) { - pre = span.attr("pre") || ""; - post = span.attr("post") || ""; - span.html(pre + value + post); + if (Array.isArray(value)) { + value.forEach(function(elem) { + span.append(elem); + span.append('
'); + }); + } else { + pre = span.attr("pre") || ""; + post = span.attr("post") || ""; + span.html(pre + value + post); + } } // Look for SELECTs @@ -1869,12 +1935,17 @@ function connectToURL(url) { initUrls(url); - $.ajax({ + fetch(urls.auth.href, { 'method': 'GET', - 'crossDomain': true, - 'url': urls.auth.href, - 'xhrFields': { 'withCredentials': true } - }).done(function(data) { + 'cors': true, + 'credentials': 'same-origin' + }).then(function(response) { + // Nothing to do, reload page and retry + if (response.status != 200) { + doReload(5000); + return; + } + // update websock object if (websock) { websock.close(); } websock = new WebSocket(urls.ws.href); websock.onmessage = function(evt) { @@ -1883,8 +1954,9 @@ function connectToURL(url) { processData(data); } }; - }).fail(function() { - // Nothing to do, reload page and retry + }).catch(function(error) { + console.log(error); + doReload(5000); }); } @@ -1900,9 +1972,11 @@ function connectToCurrentURL() { connectToURL(new URL(window.location)); } -function getParameterByName(name) { - var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search); - return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); +function enableWSLogging() { + var processDataOrig = window.processData; + window.processData = function(data) { console.log(data); processDataOrig(data); } + var sendActionOrig = window.sendAction; + window.sendAction = function(action, data) { console.log(action,data); sendActionOrig(action, data);} } $(function() { @@ -1987,7 +2061,10 @@ $(function() { if (window.location.protocol === "file:") { return; } // Check host param in query string - if (host = getParameterByName('host')) { + var search = new URLSearchParams(window.location.search), + host = search.get("host"); + + if (host !== null) { connect(host); } else { connectToCurrentURL(); diff --git a/code/html/index.html b/code/html/index.html index cccf8050..e8b42d51 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -12,7 +12,7 @@ - + @@ -512,7 +512,7 @@
-
Use forth dimmable channel as (cold) white light calculated out of the RGB values.
Will only work if the device has at least 4 dimmable channels.
Enabling this will render useless the "Channel 4" slider in the status page.
Reload the page to update the web interface.
+
For 2 channels warm white and cold white lights or color lights to use forth dimmable channel as (cold) white light calculated out of the RGB values.
Will only work if the device has at least 4 dimmable channels.
Enabling this will render useless the "Channel 4" slider in the status page.
Reload the page to update the web interface.
@@ -520,7 +520,7 @@
-
Use fifth dimmable channel as warm white light and the forth dimmable channel as cold white.
Will only work if the device has at least 5 dimmable channels and "white channel" above is also ON.
Enabling this will render useless the "Channel 5" slider in the status page.
Reload the page to update the web interface.
+
Use a dimmable channel as warm white light and another dimmable channel as cold white light.
On devices with two dimmable channels the first use used for warm white light and the second for cold white light.
On color lights the fifth use used for warm white light and the fourth for cold white light.
Will only work if the device has exactly 2 dimmable channels or at least 5 dimmable channels and "white channel" above is also ON.
Enabling this will render useless the "Channel 5" slider in the status page.
Reload the page to update the web interface.
@@ -1932,6 +1932,8 @@ + +
@@ -2080,10 +2082,10 @@ - + - + diff --git a/code/html/vendor/jquery-3.2.1.min.js b/code/html/vendor/jquery-3.2.1.min.js deleted file mode 100644 index 644d35e2..00000000 --- a/code/html/vendor/jquery-3.2.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), -a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), -null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("