From 4b3e803c22e54614d3bdf0ae7c402f0cedfaa1d4 Mon Sep 17 00:00:00 2001 From: "Alex V. Morozov" Date: Tue, 4 Aug 2020 18:44:41 +0300 Subject: [PATCH] Add files via upload --- include/Base64.h | 8 + include/Parameters.h | 223 ++++++ include/RtcFlags.h | 15 + include/StrUtils.h | 10 + platformio.ini | 21 + src/Base64.cpp | 226 ++++++ src/Parameters.cpp | 1787 ++++++++++++++++++++++++++++++++++++++++++ src/RtcFlags.cpp | 31 + src/StrUtils.cpp | 49 ++ src/main.cpp | 412 ++++++++++ 10 files changed, 2782 insertions(+) create mode 100644 include/Base64.h create mode 100644 include/Parameters.h create mode 100644 include/RtcFlags.h create mode 100644 include/StrUtils.h create mode 100644 platformio.ini create mode 100644 src/Base64.cpp create mode 100644 src/Parameters.cpp create mode 100644 src/RtcFlags.cpp create mode 100644 src/StrUtils.cpp create mode 100644 src/main.cpp diff --git a/include/Base64.h b/include/Base64.h new file mode 100644 index 0000000..6a1265d --- /dev/null +++ b/include/Base64.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +String encodeBase64(const uint8_t *data, uint16_t size); +int16_t encodeBase64(Stream &stream, const uint8_t *data, uint16_t size); +int16_t decodeBase64(const char *str, uint8_t *data, uint16_t maxsize); +int16_t decodeBase64(Stream &stream, uint8_t *data, uint16_t maxsize); diff --git a/include/Parameters.h b/include/Parameters.h new file mode 100644 index 0000000..39feb67 --- /dev/null +++ b/include/Parameters.h @@ -0,0 +1,223 @@ +#pragma once + +#include +#include +#ifdef ESP8266 +#include +#else +#include +#endif + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) +#endif + +#ifdef ESP8266 +static const char FALSE_PSTR[] PROGMEM = "false"; +static const char TRUE_PSTR[] PROGMEM = "true"; + +static const char *const BOOLS[] PROGMEM = { FALSE_PSTR, TRUE_PSTR }; +#else +static const char *const BOOLS[] = { "false", "true" }; +#endif + +enum cpevent_t : uint8_t { CP_INIT, CP_DONE, CP_RESTART, CP_WEB, CP_CONNECT, CP_DISCONNECT, CP_IDLE }; + +typedef std::function cpcallback_t; + +struct __attribute__((__packed__)) editor_t { + enum editortype_t : uint8_t { EDIT_NONE, EDIT_TEXT, EDIT_PASSWORD, EDIT_TEXTAREA, EDIT_CHECKBOX, EDIT_RADIO, EDIT_SELECT, EDIT_HIDDEN }; + + editortype_t type; + bool disabled : 1; + bool required : 1; + bool readonly : 1; + union __attribute__((__packed__)) { + struct __attribute__((__packed__)) { + uint16_t size; + uint16_t maxlength; + } text; + struct __attribute__((__packed__)) { + uint16_t cols, rows; + uint16_t maxlength; + } textarea; + struct __attribute__((__packed__)) { + const char *checkedvalue; + const char *uncheckedvalue; + } checkbox; + struct __attribute__((__packed__)) { + uint16_t count; + const char *const *values; + const char *const *titles; + } radio; + struct __attribute__((__packed__)) { + uint16_t size; + uint16_t count; + const char *const *values; + const char *const *titles; + } select; + }; +}; + +struct __attribute__((__packed__)) paraminfo_t { + enum paramtype_t : uint8_t { PARAM_BOOL, PARAM_I8, PARAM_U8, PARAM_I16, PARAM_U16, PARAM_I32, PARAM_U32, PARAM_FLOAT, PARAM_CHAR, PARAM_STR, PARAM_BINARY, PARAM_IP }; + typedef uint8_t ipaddr_t[4]; + union __attribute__((__packed__)) paramvalue_t { + bool asbool; + int32_t asint; + uint32_t asuint; + float asfloat; + char aschar; + const char *asstr; + const uint8_t *asbinary; + const ipaddr_t asip; + }; + union __attribute__((__packed__)) number_t { + int32_t asint; + uint32_t asuint; + float asfloat; + }; + + const char *name; + const char *title; + paramtype_t type; + uint16_t size; + paramvalue_t defvalue; + editor_t editor; + number_t minvalue, maxvalue; +}; + +#define EDITOR_NONE() { .type = editor_t::EDIT_NONE } +#define EDITOR_TEXT(s, m, d, r, ro) { .type = editor_t::EDIT_TEXT, .disabled = (d), .required = (r), .readonly = (ro), { .text = { .size = (s), .maxlength = (m) } } } +#define EDITOR_PASSWORD(s, m, d, r, ro) { .type = editor_t::EDIT_PASSWORD, .disabled = (d), .required = (r), .readonly = (ro), { .text = { .size = (s), .maxlength = (m) } } } +#define EDITOR_TEXTAREA(c, r, m, d, re, ro) { .type = editor_t::EDIT_TEXTAREA, .disabled = (d), .required = (re), .readonly = (ro), { .textarea = { .cols = (c), .rows = (r), .maxlength = (m) } } } +#define EDITOR_CHECKBOX(c, u, d, r, ro) { .type = editor_t::EDIT_CHECKBOX, .disabled = (d), .required = (r), .readonly = (ro), { .checkbox = { .checkedvalue = (c), .uncheckedvalue = (u) } } } +#define EDITOR_RADIO(c, v, t, d, r, ro) { .type = editor_t::EDIT_RADIO, .disabled = (d), .required = (r), .readonly = (ro), { .radio = { .count = (c), .values = (v), .titles = (t) } } } +#define EDITOR_SELECT(s, c, v, t, d, r) { .type = editor_t::EDIT_SELECT, .disabled = (d), .required = (r), .readonly = false, { .select = { .size = (s), .count = (c), .values = (v), .titles = (t) } } } +#define EDITOR_HIDDEN() { .type = editor_t::EDIT_HIDDEN } + +#define PARAM_BOOL_CUSTOM(n, t, d, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_BOOL, .size = sizeof(bool), .defvalue = { .asbool = (d) }, .editor = e } +#ifdef ESP8266 +#define PARAM_BOOL(n, t, d) PARAM_BOOL_CUSTOM(n, t, d, EDITOR_CHECKBOX(TRUE_PSTR, FALSE_PSTR, false, false, false)) +#else +#define PARAM_BOOL(n, t, d) PARAM_BOOL_CUSTOM(n, t, d, EDITOR_CHECKBOX(BOOLS[true], BOOLS[false], false, false, false)) +#endif +#define PARAM_I8_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_I8, .size = sizeof(int8_t), .defvalue = { .asint = (d) }, .editor = e, .minvalue = { .asint = (m) }, .maxvalue = { .asint = (x) } } +#define PARAM_I8(n, t, d) PARAM_I8_CUSTOM(n, t, d, -128, 127, EDITOR_TEXT(3, 4, false, false, false)) +#define PARAM_U8_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_U8, .size = sizeof(uint8_t), .defvalue = { .asuint = (d) }, .editor = e, .minvalue = { .asuint = (m) }, .maxvalue = { .asuint = (x) } } +#define PARAM_U8(n, t, d) PARAM_U8_CUSTOM(n, t, d, 0, 255, EDITOR_TEXT(3, 3, false, false, false)) +#define PARAM_I16_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_I16, .size = sizeof(int16_t), .defvalue = { .asint = (d) }, .editor = e, .minvalue = { .asint = (m) }, .maxvalue = { .asint = (x) } } +#define PARAM_I16(n, t, d) PARAM_I16_CUSTOM(n, t, d, -32768, 32767, EDITOR_TEXT(5, 6, false, false, false)) +#define PARAM_U16_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_U16, .size = sizeof(uint16_t), .defvalue = { .asuint = (d) }, .editor = e, .minvalue = { .asuint = (m) }, .maxvalue = { .asuint = (x) } } +#define PARAM_U16(n, t, d) PARAM_U16_CUSTOM(n, t, d, 0, 65535, EDITOR_TEXT(5, 5, false, false, false)) +#define PARAM_I32_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_I32, .size = sizeof(int32_t), .defvalue = { .asint = (d) }, .editor = e, .minvalue = { .asint = (m) }, .maxvalue = { .asint = (x) } } +#define PARAM_I32(n, t, d) PARAM_I32_CUSTOM(n, t, d, -2147483648L, 2147483647L, EDITOR_TEXT(10, 11, false, false, false)) +#define PARAM_U32_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_U32, .size = sizeof(uint32_t), .defvalue = { .asuint = (d) }, .editor = e, .minvalue = { .asuint = (m) }, .maxvalue = { .asuint = (x) } } +#define PARAM_U32(n, t, d) PARAM_U32_CUSTOM(n, t, d, 0, 4294967295UL, EDITOR_TEXT(10, 10, false, false, false)) +#define PARAM_FLOAT_CUSTOM(n, t, d, m, x, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_FLOAT, .size = sizeof(float), .defvalue = { .asfloat = (d) }, .editor = e, .minvalue = { .asfloat = (m) }, .maxvalue = { .asfloat = (x) } } +#define PARAM_FLOAT(n, t, d) PARAM_FLOAT_CUSTOM(n, t, d, NAN, NAN, EDITOR_TEXT(10, 15, false, false, false)) +#define PARAM_CHAR_CUSTOM(n, t, d, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_CHAR, .size = sizeof(char), .defvalue = { .aschar = (d) }, .editor = e } +#define PARAM_CHAR(n, t, d) PARAM_CHAR_CUSTOM(n, t, d, EDITOR_TEXT(1, 1, false, false, false)) +#define PARAM_STR_CUSTOM(n, t, s, d, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_STR, .size = (s), .defvalue = { .asstr = (d) }, .editor = e } +#define PARAM_STR(n, t, s, d) PARAM_STR_CUSTOM(n, t, s, d, EDITOR_TEXT(((s) > 33 ? 32 : ((s) - 1)), ((s) - 1), false, false, false)) +#define PARAM_PASSWORD(n, t, s, d) PARAM_STR_CUSTOM(n, t, s, d, EDITOR_PASSWORD(((s) > 33 ? 32 : ((s) - 1)), ((s) - 1), false, false, false)) +#define PARAM_BINARY_CUSTOM(n, t, s, d, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_BINARY, .size = (s), .defvalue = { .asbinary = (d) }, .editor = e } +#define PARAM_BINARY(n, t, s, d) PARAM_BINARY_CUSTOM(n, t, s, d, EDITOR_TEXT((((s) + 2) / 3 * 4) > 32 ? 32 : (((s) + 2) / 3 * 4), (((s) + 2) / 3 * 4), false, false, false)) +#define PARAM_IP_CUSTOM(n, t, d1, d2, d3, d4, e) { .name = (n), .title = (t), .type = paraminfo_t::PARAM_IP, .size = sizeof(paraminfo_t::ipaddr_t), .defvalue = { .asip = { (d1), (d2), (d3), (d4) } }, .editor = e } +#define PARAM_IP(n, t, d1, d2, d3, d4) PARAM_IP_CUSTOM(n, t, d1, d2, d3, d4, EDITOR_TEXT(15, 15, false, false, false)) + +class Parameters { +public: + Parameters(const paraminfo_t *params, uint16_t cnt) : _params(params), _count(cnt), _inited(false) {} + + bool begin(); + operator bool() { + return _inited; + } + uint16_t count() const { + return _count; + } + int16_t find(const char *name) const; + const paraminfo_t *getInfo(uint16_t index) const; + const char *name(uint16_t index) const; + paraminfo_t::paramtype_t type(uint16_t index) const; + uint16_t size(uint16_t index) const; + uint16_t size(const char *name) const { + return size(find(name)); + } +#ifdef ESP8266 + const void *value(uint16_t index) const; +#else + const void *value(uint16_t index) const { + return getPtr(index); + } +#endif + const void *value(const char *name) const { + return value(find(name)); + } + bool update(); + bool clear(); + void clear(uint16_t index); + void clear(const char *name) { + clear(find(name)); + } + bool get(uint16_t index, void *data, uint16_t maxsize); + bool get(const char *name, void *data, uint16_t maxsize) { + return get(find(name), data, maxsize); + } + bool set(uint16_t index, const void *data); + bool set(const char *name, const void *data) { + return set(find(name), data); + } + String toString(uint16_t index, bool encode = false); + String toString(const char *name, bool encode = false) { + return toString(find(name), encode); + } + int16_t toStream(uint16_t index, Stream &stream, bool encode = false); + int16_t toStream(const char *name, Stream &stream, bool encode = false) { + return toStream(find(name), stream, encode); + } + bool fromString(uint16_t index, const String &str); + bool fromString(const char *name, const String &str) { + return fromString(find(name), str); + } + bool fromStream(uint16_t index, Stream &stream); + bool fromStream(const char *name, Stream &stream) { + return fromStream(find(name), stream); + } + +#ifdef ESP8266 + void handleWebPage(ESP8266WebServer &http, const char *restartPath = NULL, bool confirmation = true); +#else + void handleWebPage(WebServer &http, const char *restartPath = NULL, bool confirmation = true); +#endif + +protected: + static const uint16_t EEPROM_SIGN = 0xA55A; + + struct __attribute__((__packed__)) header_t { + uint16_t sign; + uint16_t crc; + }; + + static uint16_t crc16(uint8_t data, uint16_t crc = 0xFFFF); + static uint16_t crc16(const uint8_t *data, uint16_t size, uint16_t crc = 0xFFFF); + + void *getPtr(uint16_t index) const; + bool check(); + + String getScript(); + String getEditor(uint16_t index); + + String encodeString(char c); + String encodeString(const char *str); + + const paraminfo_t *_params; +#ifdef ESP8266 + uint8_t _alignedData[4]; +#endif + uint16_t _count : 15; + bool _inited : 1; +}; + +bool paramsCaptivePortal(Parameters *params, const char *ssid, const char *pswd, uint16_t duration = 0, cpcallback_t callback = NULL); diff --git a/include/RtcFlags.h b/include/RtcFlags.h new file mode 100644 index 0000000..a745440 --- /dev/null +++ b/include/RtcFlags.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class RtcFlags { +public: + static uint16_t getFlags(); + static bool getFlag(uint8_t flag); + static bool setFlags(uint16_t flags); + static bool setFlag(uint8_t flag); + static bool clearFlag(uint8_t flag); + +protected: + static const uint32_t RTC_SIGN = 0xA1B2C3D4; +}; diff --git a/include/StrUtils.h b/include/StrUtils.h new file mode 100644 index 0000000..5c2b650 --- /dev/null +++ b/include/StrUtils.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +String printfToString(const char *fmt, ...); + +int8_t strcmp_PP(const char *s1, const char *s2); +int8_t strncmp_PP(const char *s1, const char *s2, uint16_t maxlen); + +int sscanf_P(const char *str, const char *fmt, ...); diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..103cf23 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,21 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp01_1m] +platform = espressif8266 +board = esp01_1m +framework = arduino +;upload_speed = 921600 +;monitor_speed = 74880 +monitor_speed = 115200 +;board_build.flash_mode = dout + +lib_deps = + PubSubClient diff --git a/src/Base64.cpp b/src/Base64.cpp new file mode 100644 index 0000000..23121ac --- /dev/null +++ b/src/Base64.cpp @@ -0,0 +1,226 @@ +#ifdef ESP8266 +#include +#endif +#include "Base64.h" + +static bool isBase64(char c) { + return ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) || (c == '+') || (c == '/'); +} + +static char encodeByte(uint8_t b) { + if (b < 26) + return 'A' + b; + if (b < 52) + return 'a' + b - 26; + if (b < 62) + return '0' + b - 52; + if (b == 62) + return '+'; + if (b == 63) + return '/'; +// return '='; +} + +static uint8_t decodeByte(char c) { + if ((c >= 'A') && (c <= 'Z')) + return c - 'A'; + if ((c >= 'a') && (c <= 'z')) + return c - 'a' + 26; + if ((c >= '0') && (c <= '9')) + return c - '0' + 52; + if (c == '+') + return 62; + if (c == '/') + return 63; +// return 0; +} + +String encodeBase64(const uint8_t *data, uint16_t size) { + String result; + uint32_t buffer = 0; + + if (result.reserve((size + 2) / 3 * 4)) { + while (size >= 3) { + buffer = 0; + for (int8_t i = 2; i >= 0; --i) { +#ifdef ESP8266 + buffer |= pgm_read_byte(data++) << (i * 8); +#else + buffer |= *data++ << (i * 8); +#endif + } + for (int8_t i = 3; i >= 0; --i) { + result.concat(encodeByte((buffer >> (i * 6)) & 0x3F)); + } + size -= 3; + } + if (size) { +#ifdef ESP8266 + buffer = pgm_read_byte(data++) << 16; +#else + buffer = *data++ << 16; +#endif + if (size > 1) +#ifdef ESP8266 + buffer |= pgm_read_byte(data) << 8; +#else + buffer |= *data << 8; +#endif + result.concat(encodeByte((buffer >> 18) & 0x3F)); + result.concat(encodeByte((buffer >> 12) & 0x3F)); + if (size > 1) + result.concat(encodeByte((buffer >> 6) & 0x3F)); + result.concat('='); + if (size == 1) + result.concat('='); + } + } + return result; +} + +int16_t encodeBase64(Stream &stream, const uint8_t *data, uint16_t size) { + uint16_t result = 0; + uint32_t buffer = 0; + + while (size >= 3) { + buffer = 0; + for (int8_t i = 2; i >= 0; --i) { +#ifdef ESP8266 + buffer |= pgm_read_byte(data++) << (i * 8); +#else + buffer |= *data++ << (i * 8); +#endif + } + for (int8_t i = 3; i >= 0; --i) { + if (stream.print(encodeByte((buffer >> (i * 6)) & 0x3F)) != sizeof(char)) + return -1; + result += sizeof(char); + } + size -= 3; + } + if (size) { +#ifdef ESP8266 + buffer = pgm_read_byte(data++) << 16; +#else + buffer = *data++ << 16; +#endif + if (size > 1) +#ifdef ESP8266 + buffer |= pgm_read_byte(data) << 8; +#else + buffer |= *data << 8; +#endif + if (stream.print(encodeByte((buffer >> 18) & 0x3F)) != sizeof(char)) + return -1; + result += sizeof(char); + if (stream.print(encodeByte((buffer >> 12) & 0x3F)) != sizeof(char)) + return -1; + result += sizeof(char); + if (size > 1) { + if (stream.print(encodeByte((buffer >> 6) & 0x3F)) != sizeof(char)) + return -1; + result += sizeof(char); + } + if (stream.print('=') != sizeof(char)) + return -1; + result += sizeof(char); + if (size == 1) { + if (stream.print('=') != sizeof(char)) + return -1; + result += sizeof(char); + } + } + return result; +} + +int16_t decodeBase64(const char *str, uint8_t *data, uint16_t maxsize) { + uint16_t result = 0; + uint32_t buffer = 0; + uint8_t buflen = 0; + +#ifdef ESP8266 + while (pgm_read_byte(str) && maxsize) { +#else + while (*str && maxsize) { +#endif +#ifdef ESP8266 + if (pgm_read_byte(str) == '=') +#else + if (*str == '=') +#endif + break; +#ifdef ESP8266 + if (! isBase64(pgm_read_byte(str))) +#else + if (! isBase64(*str)) +#endif + return -1; +#ifdef ESP8266 + buffer |= decodeByte(pgm_read_byte(str++)) << ((3 - buflen) * 6); +#else + buffer |= decodeByte(*str++) << ((3 - buflen) * 6); +#endif + if (++buflen >= 4) { + for (int8_t i = 2; i >= 0; --i) { + if (maxsize) { + *data++ = buffer >> (i * 8); + --maxsize; + ++result; + } else + break; + } + buffer = 0; + buflen = 0; + } + } + if (maxsize && buflen) { + *data++ = buffer >> 16; + --maxsize; + ++result; + if (maxsize && (buflen > 2)) { + *data = buffer >> 8; +// --maxsize; + ++result; + } + } + return result; +} + +int16_t decodeBase64(Stream &stream, uint8_t *data, uint16_t maxsize) { + uint16_t result = 0; + uint32_t buffer = 0; + uint8_t buflen = 0; + + while (stream.available() && maxsize) { + char c = stream.read(); + + if (c == '=') + break; + if (! isBase64(c)) + return -1; + buffer |= decodeByte(c) << ((3 - buflen) * 6); + if (++buflen >= 4) { + for (int8_t i = 2; i >= 0; --i) { + if (maxsize) { + *data++ = buffer >> (i * 8); + --maxsize; + ++result; + } else + break; + } + buffer = 0; + buflen = 0; + } + } + if (maxsize && buflen) { + *data++ = buffer >> 16; + --maxsize; + ++result; + if (maxsize && (buflen > 2)) { + *data = buffer >> 8; +// --maxsize; + ++result; + } + } + return result; +} diff --git a/src/Parameters.cpp b/src/Parameters.cpp new file mode 100644 index 0000000..01606a1 --- /dev/null +++ b/src/Parameters.cpp @@ -0,0 +1,1787 @@ +#include +#include +#ifdef ESP32 +#include +#endif +#include +#ifdef ESP8266 +#include +#else +#include +#endif +#include +#include +#include "Parameters.h" +#ifdef ESP8266 +#include "StrUtils.h" +#endif +#include "Base64.h" + +#ifdef ESP32 +static const char TAG[] = "Parameters"; +#endif + +#ifdef ESP8266 +static const char EMPTY_PSTR[] PROGMEM = ""; + +static const char FMT_I8_PSTR[] PROGMEM = "%hhd"; +static const char FMT_U8_PSTR[] PROGMEM = "%hhu"; +static const char FMT_I16_PSTR[] PROGMEM = "%hd"; +static const char FMT_U16_PSTR[] PROGMEM = "%hu"; +static const char FMT_I32_PSTR[] PROGMEM = "%d"; +static const char FMT_U32_PSTR[] PROGMEM = "%u"; +static const char FMT_FLOAT_PSTR[] PROGMEM = "%f"; +static const char FMT_IP_PSTR[] PROGMEM = "%hhu.%hhu.%hhu.%hhu"; + +static const char QUOT_PSTR[] PROGMEM = """; +static const char LT_PSTR[] PROGMEM = "<"; +static const char GT_PSTR[] PROGMEM = ">"; + +static const char TEXTHTML_PSTR[] PROGMEM = "text/html"; +static const char TEXTPLAIN_PSTR[] PROGMEM = "text/plain"; +#else +static const char EMPTY_PSTR[] = ""; + +static const char FMT_I8_PSTR[] = "%hhd"; +static const char FMT_U8_PSTR[] = "%hhu"; +static const char FMT_I16_PSTR[] = "%hd"; +static const char FMT_U16_PSTR[] = "%hu"; +static const char FMT_I32_PSTR[] = "%d"; +static const char FMT_U32_PSTR[] = "%u"; +static const char FMT_FLOAT_PSTR[] = "%f"; +static const char FMT_IP_PSTR[] = "%hhu.%hhu.%hhu.%hhu"; + +static const char QUOT_PSTR[] = """; +static const char LT_PSTR[] = "<"; +static const char GT_PSTR[] = ">"; + +static const char TEXTHTML_PSTR[] = "text/html"; +static const char TEXTPLAIN_PSTR[] = "text/plain"; +#endif + +bool Parameters::begin() { + if (! _inited) { + uint16_t size = sizeof(header_t); + + for (uint16_t i = 0; i < _count; ++i) { +#ifdef ESP8266 + size += pgm_read_word(&_params[i].size); +#else + size += _params[i].size; +#endif + } +#ifdef ESP8266 + EEPROM.begin(size); + _inited = true; +#else + _inited = EEPROM.begin(size); + if (! _inited) { + ESP_LOGE(TAG, "Error allocating of EEPROM (%hu bytes)!", size); + return false; + } +#endif + if (! check()) { + clear(); +#ifdef ESP32 + ESP_LOGW(TAG, "Reset EEPROM parameters!"); +#endif + } + } + return _inited; +} + +int16_t Parameters::find(const char *name) const { + for (uint16_t i = 0; i < _count; ++i) { +#ifdef ESP8266 + if (! strcmp_PP((char*)pgm_read_ptr(&_params[i].name), name)) +#else + if (! strcmp(_params[i].name, name)) +#endif + return i; + } + return -1; +} + +const paraminfo_t *Parameters::getInfo(uint16_t index) const { + if (index < _count) + return &_params[index]; + return NULL; +} + +const char *Parameters::name(uint16_t index) const { + if (index < _count) +#ifdef ESP8266 + return (char*)pgm_read_ptr(&_params[index].name); +#else + return _params[index].name; +#endif + return NULL; +} + +paraminfo_t::paramtype_t Parameters::type(uint16_t index) const { + if (index < _count) +#ifdef ESP8266 + return (paraminfo_t::paramtype_t)pgm_read_byte(&_params[index].type); +#else + return _params[index].type; +#endif +} + +uint16_t Parameters::size(uint16_t index) const { + if (index < _count) +#ifdef ESP8266 + return pgm_read_word(&_params[index].size); +#else + return _params[index].size; +#endif + return 0; +} + +#ifdef ESP8266 +const void *Parameters::value(uint16_t index) const { + const void *result = NULL; + + if (_inited && (index < _count)) { + result = getPtr(index); + + if (result) { + paraminfo_t::paramtype_t type = (paraminfo_t::paramtype_t)pgm_read_byte(&_params[index].type); + uint16_t size = pgm_read_word(&_params[index].size); + + if ((size > 1) && ((type != paraminfo_t::PARAM_STR) && (type != paraminfo_t::PARAM_BINARY))) { + memcpy((void*)_alignedData, result, size); + result = _alignedData; + } + } + } + return result; +} +#endif + +bool Parameters::clear() { + for (uint16_t i = 0; i < _count; ++i) { + clear(i); + } + return update(); +} + +void Parameters::clear(uint16_t index) { + if (_inited && (index < _count)) { +#ifdef ESP8266 + switch (pgm_read_byte(&_params[index].type)) { +#else + switch (_params[index].type) { +#endif + case paraminfo_t::PARAM_BOOL: + case paraminfo_t::PARAM_U8: + case paraminfo_t::PARAM_U16: + case paraminfo_t::PARAM_I32: + case paraminfo_t::PARAM_U32: + case paraminfo_t::PARAM_FLOAT: + case paraminfo_t::PARAM_CHAR: + set(index, &_params[index].defvalue); + break; + case paraminfo_t::PARAM_I8: + { +#ifdef ESP8266 + int8_t i8 = (int32_t)pgm_read_dword(&_params[index].defvalue.asint); +#else + int8_t i8 = _params[index].defvalue.asint; +#endif + + set(index, &i8); + } + break; + case paraminfo_t::PARAM_I16: + { +#ifdef ESP8266 + int16_t i16 = (int32_t)pgm_read_dword(&_params[index].defvalue.asint); +#else + int16_t i16 = _params[index].defvalue.asint; +#endif + + set(index, &i16); + } + break; + case paraminfo_t::PARAM_STR: +#ifdef ESP8266 + set(index, pgm_read_ptr(&_params[index].defvalue.asstr)); +#else + set(index, _params[index].defvalue.asstr); +#endif + break; + case paraminfo_t::PARAM_BINARY: +#ifdef ESP8266 + set(index, pgm_read_ptr(&_params[index].defvalue.asbinary)); +#else + set(index, _params[index].defvalue.asbinary); +#endif + break; + case paraminfo_t::PARAM_IP: +#ifdef ESP8266 + set(index, &_params[index].defvalue.asip[0]); +#else + set(index, _params[index].defvalue.asip); +#endif + break; + } + } +} + +bool Parameters::get(uint16_t index, void *data, uint16_t maxsize) { + if (_inited && (index < _count) && data && maxsize) { + const void *ptr = getPtr(index); + + if (ptr) { +#ifdef ESP8266 + paraminfo_t::paramtype_t type = (paraminfo_t::paramtype_t)pgm_read_byte(&_params[index].type); + uint16_t size = pgm_read_word(&_params[index].size); + + if ((type < paraminfo_t::PARAM_STR) || (type == paraminfo_t::PARAM_IP)) { + if (maxsize >= size) { + memcpy(data, ptr, size); +#else + if ((_params[index].type < paraminfo_t::PARAM_STR) || (_params[index].type == paraminfo_t::PARAM_IP)) { + if (maxsize >= _params[index].size) { + memcpy(data, ptr, _params[index].size); +#endif + return true; + } + } else { // type == PARAM_STR or PARAM_BINARY +#ifdef ESP8266 + if (maxsize > size) + maxsize = size; + if (type == paraminfo_t::PARAM_STR) { +#else + if (maxsize > _params[index].size) + maxsize = _params[index].size; + if (_params[index].type == paraminfo_t::PARAM_STR) { +#endif + memcpy(data, ptr, maxsize - 1); + ((char*)data)[maxsize - 1] = '\0'; + } else { // type == PARAM_BINARY + memcpy(data, ptr, maxsize); + } + return true; + } + } + } + return false; +} + +bool Parameters::set(uint16_t index, const void *data) { + if (_inited && (index < _count)) { + void *ptr = getPtr(index); + + if (ptr) { +#ifdef ESP8266 + uint16_t size = pgm_read_word(&_params[index].size); + + memset(ptr, 0, size); +#else + memset(ptr, 0, _params[index].size); +#endif + if (data) { +#ifdef ESP8266 + if (pgm_read_byte(&_params[index].type) != paraminfo_t::PARAM_STR) { + memcpy_P(ptr, data, size); + } else { // type == PARAM_STR + strncpy_P((char*)ptr, (char*)data, size - 1); +// ((char*)ptr)[size - 1] = '\0'; +#else + if (_params[index].type != paraminfo_t::PARAM_STR) { + memcpy(ptr, data, _params[index].size); + } else { // type == PARAM_STR + strncpy((char*)ptr, (char*)data, _params[index].size - 1); +// ((char*)ptr)[_params[index].size - 1] = '\0'; +#endif + } + } + return true; + } + } + return false; +} + +String Parameters::toString(uint16_t index, bool encode) { + if (_inited && (index < _count)) { + StreamString result; + + if (toStream(index, result, encode) >= 0) + return result; + } + return String(); +} + +#ifdef ESP8266 +int16_t Parameters::toStream(uint16_t index, Stream &stream, bool encode) { + int16_t result = -1; + + if (_inited && (index < _count)) { + const void *ptr = value(index); + + if (ptr) { + switch (pgm_read_byte(&_params[index].type)) { + case paraminfo_t::PARAM_BOOL: + if (*(bool*)ptr) + result = stream.print(FPSTR((char*)pgm_read_ptr(&BOOLS[true]))); + else + result = stream.print(FPSTR((char*)pgm_read_ptr(&BOOLS[false]))); + break; + case paraminfo_t::PARAM_I8: + result = stream.printf_P(FMT_I8_PSTR, *(int8_t*)ptr); + break; + case paraminfo_t::PARAM_U8: + result = stream.printf_P(FMT_U8_PSTR, *(uint8_t*)ptr); + break; + case paraminfo_t::PARAM_I16: + result = stream.printf_P(FMT_I16_PSTR, *(int16_t*)ptr); + break; + case paraminfo_t::PARAM_U16: + result = stream.printf_P(FMT_U16_PSTR, *(uint16_t*)ptr); + break; + case paraminfo_t::PARAM_I32: + result = stream.printf_P(FMT_I32_PSTR, *(int32_t*)ptr); + break; + case paraminfo_t::PARAM_U32: + result = stream.printf_P(FMT_U32_PSTR, *(uint32_t*)ptr); + break; + case paraminfo_t::PARAM_FLOAT: + result = stream.printf_P(FMT_FLOAT_PSTR, *(float*)ptr); + break; + case paraminfo_t::PARAM_CHAR: + if (encode) + result = stream.print(encodeString(*(char*)ptr)); + else + result = stream.print(*(char*)ptr); + break; + case paraminfo_t::PARAM_STR: + if (encode) + result = stream.print(encodeString((char*)ptr)); + else + result = stream.print((char*)ptr); + break; + case paraminfo_t::PARAM_BINARY: + result = encodeBase64(stream, (uint8_t*)ptr, pgm_read_word(&_params[index].size)); + break; + case paraminfo_t::PARAM_IP: + result = stream.printf_P(FMT_IP_PSTR, (*(paraminfo_t::ipaddr_t*)ptr)[0], (*(paraminfo_t::ipaddr_t*)ptr)[1], + (*(paraminfo_t::ipaddr_t*)ptr)[2], (*(paraminfo_t::ipaddr_t*)ptr)[3]); + break; + } + } + } + return result; +} + +#else +int16_t Parameters::toStream(uint16_t index, Stream &stream, bool encode) { + int16_t result = -1; + + if (_inited && (index < _count)) { + const void *ptr = getPtr(index); + + if (ptr) { + switch (_params[index].type) { + case paraminfo_t::PARAM_BOOL: + if (*(bool*)ptr) + result = stream.print(BOOLS[true]); + else + result = stream.print(BOOLS[false]); + break; + case paraminfo_t::PARAM_I8: + result = stream.printf(FMT_I8_PSTR, *(int8_t*)ptr); + break; + case paraminfo_t::PARAM_U8: + result = stream.printf(FMT_U8_PSTR, *(uint8_t*)ptr); + break; + case paraminfo_t::PARAM_I16: + result = stream.printf(FMT_I16_PSTR, *(int16_t*)ptr); + break; + case paraminfo_t::PARAM_U16: + result = stream.printf(FMT_U16_PSTR, *(uint16_t*)ptr); + break; + case paraminfo_t::PARAM_I32: + result = stream.printf(FMT_I32_PSTR, *(int32_t*)ptr); + break; + case paraminfo_t::PARAM_U32: + result = stream.printf(FMT_U32_PSTR, *(uint32_t*)ptr); + break; + case paraminfo_t::PARAM_FLOAT: + result = stream.printf(FMT_FLOAT_PSTR, *(float*)ptr); + break; + case paraminfo_t::PARAM_CHAR: + if (encode) + result = stream.print(encodeString(*(char*)ptr)); + else + result = stream.print(*(char*)ptr); + break; + case paraminfo_t::PARAM_STR: + if (encode) + result = stream.print(encodeString((char*)ptr)); + else + result = stream.print((char*)ptr); + break; + case paraminfo_t::PARAM_BINARY: + result = encodeBase64(stream, (uint8_t*)ptr, _params[index].size); + break; + case paraminfo_t::PARAM_IP: + result = stream.printf(FMT_IP_PSTR, (*(paraminfo_t::ipaddr_t*)ptr)[0], (*(paraminfo_t::ipaddr_t*)ptr)[1], + (*(paraminfo_t::ipaddr_t*)ptr)[2], (*(paraminfo_t::ipaddr_t*)ptr)[3]); + break; + } + } + } + return result; +} +#endif + +#ifdef ESP8266 +bool Parameters::fromString(uint16_t index, const String &str) { + bool result = false; + + if (_inited && (index < _count)) { + void *ptr = getPtr(index); + + if (ptr) { + uint8_t data[4]; + + switch (pgm_read_byte(&_params[index].type)) { + case paraminfo_t::PARAM_BOOL: + if (str.equals(FPSTR((char*)pgm_read_ptr(&BOOLS[true]))) || str.equals(F("1"))) { + *(bool*)ptr = true; + result = true; + } else if (str.equals(FPSTR((char*)pgm_read_ptr(&BOOLS[false]))) || str.equals(F("0"))) { + *(bool*)ptr = false; + result = true; + } + break; + case paraminfo_t::PARAM_I8: + result = sscanf_P(str.c_str(), FMT_I8_PSTR, (int8_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_U8: + result = sscanf_P(str.c_str(), FMT_U8_PSTR, (uint8_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_I16: + result = sscanf_P(str.c_str(), FMT_I16_PSTR, (int16_t*)data) == 1; + if (result) + set(index, data); + break; + case paraminfo_t::PARAM_U16: + result = sscanf_P(str.c_str(), FMT_U16_PSTR, (uint16_t*)data) == 1; + if (result) + set(index, data); + break; + case paraminfo_t::PARAM_I32: + result = sscanf_P(str.c_str(), FMT_I32_PSTR, (int32_t*)data) == 1; + if (result) + set(index, data); + break; + case paraminfo_t::PARAM_U32: + result = sscanf_P(str.c_str(), FMT_U32_PSTR, (uint32_t*)data) == 1; + if (result) + set(index, data); + break; + case paraminfo_t::PARAM_FLOAT: + result = sscanf_P(str.c_str(), FMT_FLOAT_PSTR, (float*)data) == 1; + if (result) + set(index, data); + break; + case paraminfo_t::PARAM_CHAR: + *(char*)ptr = str[0]; + result = true; + break; + case paraminfo_t::PARAM_STR: + strncpy((char*)ptr, str.c_str(), pgm_read_word(&_params[index].size) - 1); + ((char*)ptr)[pgm_read_word(&_params[index].size) - 1] = '\0'; + result = true; + break; + case paraminfo_t::PARAM_BINARY: + result = decodeBase64(str.c_str(), (uint8_t*)ptr, pgm_read_word(&_params[index].size)) != -1; + break; + case paraminfo_t::PARAM_IP: + result = sscanf_P(str.c_str(), FMT_IP_PSTR, &data[0], &data[1], &data[2], &data[3]); + if (result) + set(index, data); + break; + } + if (! result) { + clear(index); + } + } + } + return result; +} + +#else +bool Parameters::fromString(uint16_t index, const String &str) { + bool result = false; + + if (_inited && (index < _count)) { + void *ptr = getPtr(index); + + if (ptr) { + switch (_params[index].type) { + case paraminfo_t::PARAM_BOOL: + if (str.equals(BOOLS[true]) || str.equals("1")) { + *(bool*)ptr = true; + result = true; + } else if (str.equals(BOOLS[false]) || str.equals("0")) { + *(bool*)ptr = false; + result = true; + } + break; + case paraminfo_t::PARAM_I8: + result = sscanf(str.c_str(), FMT_I8_PSTR, (int8_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_U8: + result = sscanf(str.c_str(), FMT_U8_PSTR, (uint8_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_I16: + result = sscanf(str.c_str(), FMT_I16_PSTR, (int16_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_U16: + result = sscanf(str.c_str(), FMT_U16_PSTR, (uint16_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_I32: + result = sscanf(str.c_str(), FMT_I32_PSTR, (int32_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_U32: + result = sscanf(str.c_str(), FMT_U32_PSTR, (uint32_t*)ptr) == 1; + break; + case paraminfo_t::PARAM_FLOAT: + result = sscanf(str.c_str(), FMT_FLOAT_PSTR, (float*)ptr) == 1; + break; + case paraminfo_t::PARAM_CHAR: + *(char*)ptr = str[0]; + result = true; + break; + case paraminfo_t::PARAM_STR: + strncpy((char*)ptr, str.c_str(), _params[index].size - 1); + ((char*)ptr)[_params[index].size - 1] = '\0'; + result = true; + break; + case paraminfo_t::PARAM_BINARY: + result = decodeBase64(str.c_str(), (uint8_t*)ptr, _params[index].size) != -1; + break; + case paraminfo_t::PARAM_IP: + result = sscanf(str.c_str(), FMT_IP_PSTR, &(*(paraminfo_t::ipaddr_t*)ptr)[0], &(*(paraminfo_t::ipaddr_t*)ptr)[1], + &(*(paraminfo_t::ipaddr_t*)ptr)[2], &(*(paraminfo_t::ipaddr_t*)ptr)[3]) == 4; + break; + } + if (! result) { + clear(index); + } + } + } + return result; +} +#endif + +bool Parameters::fromStream(uint16_t index, Stream &stream) { + if (_inited && (index < _count)) { + return fromString(index, stream.readString()); + } + return false; +} + +#ifdef ESP8266 +String Parameters::getScript() { + return String(F("\n")); +} +#else +String Parameters::getScript() { + return String("\n"); +} +#endif + +#ifdef ESP8266 +String Parameters::getEditor(uint16_t index) { + static const char DISABLED_PSTR[] PROGMEM = " disabled"; + static const char REQUIRED_PSTR[] PROGMEM = " required"; + static const char READONLY_PSTR[] PROGMEM = " readonly"; + static const char CHECKED_PSTR[] PROGMEM = " checked"; + static const char SELECTED_PSTR[] PROGMEM = " selected"; + static const char SIZE_PSTR[] PROGMEM = " size="; + static const char MAXLENGTH_PSTR[] PROGMEM = " maxlength="; + static const char NAN_PSTR[] PROGMEM = "NaN"; + + if (_inited && (index < _count) && (pgm_read_byte(&_params[index].editor.type) != editor_t::EDIT_NONE) && getPtr(index)) { + StreamString result; + editor_t editor; + + memcpy_P(&editor, &_params[index].editor, sizeof(editor_t)); + if (editor.type == editor_t::EDIT_SELECT) { + result.print(F("")); + } else if (editor.type == editor_t::EDIT_RADIO) { + if (editor.radio.count && editor.radio.values) { + for (uint16_t i = 0; i < editor.radio.count; ++i) { + result.print(F("'); + if (editor.radio.titles) + result.print(encodeString((char*)pgm_read_ptr(&editor.radio.titles[i]))); + else + result.print(encodeString((char*)pgm_read_ptr(&editor.radio.values[i]))); + result.print('\n'); + } + } + } else if (editor.type == editor_t::EDIT_TEXTAREA) { + result.print(F("")); + } else { + result.print(F("'); + } + if ((editor.type == editor_t::EDIT_TEXT) || (editor.type == editor_t::EDIT_PASSWORD) || + (editor.type == editor_t::EDIT_HIDDEN)) { + result.print(F(" value=\"")); + toStream(index, result, true); + result.print('"'); + if (editor.type == editor_t::EDIT_TEXT) { + if (! editor.readonly) { + paraminfo_t::paramtype_t type = (paraminfo_t::paramtype_t)pgm_read_byte(&_params[index].type); + + if ((type >= paraminfo_t::PARAM_I8) && (type <= paraminfo_t::PARAM_U32)) { // Integers + result.print(F(" onblur=\"checkInt(this,")); + if ((type == paraminfo_t::PARAM_I8) || (type == paraminfo_t::PARAM_I16) || + (type == paraminfo_t::PARAM_I32)) { + result.printf_P(FMT_I32_PSTR, (int32_t)pgm_read_dword(&_params[index].defvalue.asint)); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf_P(FMT_U32_PSTR, pgm_read_dword(&_params[index].defvalue.asuint)); + } + result.print(','); + if ((type == paraminfo_t::PARAM_I8) || (type == paraminfo_t::PARAM_I16) || + (type == paraminfo_t::PARAM_I32)) { + result.printf_P(FMT_I32_PSTR, (int32_t)pgm_read_dword(&_params[index].minvalue.asint)); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf_P(FMT_U32_PSTR, pgm_read_dword(&_params[index].minvalue.asuint)); + } + result.print(','); + if ((type == paraminfo_t::PARAM_I8) || (type == paraminfo_t::PARAM_I16) || + (type == paraminfo_t::PARAM_I32)) { + result.printf_P(FMT_I32_PSTR, (int32_t)pgm_read_dword(&_params[index].maxvalue.asint)); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf_P(FMT_U32_PSTR, pgm_read_dword(&_params[index].maxvalue.asuint)); + } + result.print(F(");\"")); + } else if (type == paraminfo_t::PARAM_FLOAT) { + result.print(F(" onblur=\"checkFloat(this,")); + if (isnan(pgm_read_float(&_params[index].defvalue.asfloat))) { + result.print(FPSTR(NAN_PSTR)); + } else { + result.printf_P(FMT_FLOAT_PSTR, pgm_read_float(&_params[index].defvalue.asfloat)); + } + result.print(','); + if (isnan(pgm_read_float(&_params[index].minvalue.asfloat))) { + result.print(FPSTR(NAN_PSTR)); + } else { + result.printf_P(FMT_FLOAT_PSTR, pgm_read_float(&_params[index].minvalue.asfloat)); + } + result.print(','); + if (isnan(pgm_read_float(&_params[index].maxvalue.asfloat))) { + result.print(FPSTR(NAN_PSTR)); + } else { + result.printf_P(FMT_FLOAT_PSTR, pgm_read_float(&_params[index].maxvalue.asfloat)); + } + result.print(F(");\"")); + } + } + } + result.print('>'); + } + } + return result; + } + return String(); +} + +#else +String Parameters::getEditor(uint16_t index) { + static const char DISABLED_PSTR[] = " disabled"; + static const char REQUIRED_PSTR[] = " required"; + static const char READONLY_PSTR[] = " readonly"; + static const char CHECKED_PSTR[] = " checked"; + static const char SELECTED_PSTR[] = " selected"; + static const char SIZE_PSTR[] = " size="; + static const char MAXLENGTH_PSTR[] = " maxlength="; + static const char NAN_PSTR[] = "NaN"; + + if (_inited && (index < _count) && (_params[index].editor.type != editor_t::EDIT_NONE) && getPtr(index)) { + StreamString result; + + if (_params[index].editor.type == editor_t::EDIT_SELECT) { + result.print(""); + } else if (_params[index].editor.type == editor_t::EDIT_RADIO) { + if (_params[index].editor.radio.count && _params[index].editor.radio.values) { + for (uint16_t i = 0; i < _params[index].editor.radio.count; ++i) { + result.print("'); + if (_params[index].editor.radio.titles) + result.print(encodeString(_params[index].editor.radio.titles[i])); + else + result.print(encodeString(_params[index].editor.radio.values[i])); + result.print('\n'); + } + } + } else if (_params[index].editor.type == editor_t::EDIT_TEXTAREA) { + result.print(""); + } else { + result.print("'); + } + if ((_params[index].editor.type == editor_t::EDIT_TEXT) || (_params[index].editor.type == editor_t::EDIT_PASSWORD) || + (_params[index].editor.type == editor_t::EDIT_HIDDEN)) { + result.print(" value=\""); + toStream(index, result, true); + result.print('"'); + if (_params[index].editor.type == editor_t::EDIT_TEXT) { + if (! _params[index].editor.readonly) { + if ((_params[index].type >= paraminfo_t::PARAM_I8) && (_params[index].type <= paraminfo_t::PARAM_U32)) { // Integers + result.print(" onblur=\"checkInt(this,"); + if ((_params[index].type == paraminfo_t::PARAM_I8) || (_params[index].type == paraminfo_t::PARAM_I16) || + (_params[index].type == paraminfo_t::PARAM_I32)) { + result.printf(FMT_I32_PSTR, _params[index].defvalue.asint); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf(FMT_U32_PSTR, _params[index].defvalue.asuint); + } + result.print(','); + if ((_params[index].type == paraminfo_t::PARAM_I8) || (_params[index].type == paraminfo_t::PARAM_I16) || + (_params[index].type == paraminfo_t::PARAM_I32)) { + result.printf(FMT_I32_PSTR, _params[index].minvalue.asint); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf(FMT_U32_PSTR, _params[index].minvalue.asuint); + } + result.print(','); + if ((_params[index].type == paraminfo_t::PARAM_I8) || (_params[index].type == paraminfo_t::PARAM_I16) || + (_params[index].type == paraminfo_t::PARAM_I32)) { + result.printf(FMT_I32_PSTR, _params[index].maxvalue.asint); + } else { // type == PARAM_U8 or PARAM_U16 or PARAM_U32 + result.printf(FMT_U32_PSTR, _params[index].maxvalue.asuint); + } + result.print(");\""); + } else if (_params[index].type == paraminfo_t::PARAM_FLOAT) { + result.print(" onblur=\"checkFloat(this,"); + if (isnan(_params[index].defvalue.asfloat)) { + result.print(NAN_PSTR); + } else { + result.printf(FMT_FLOAT_PSTR, _params[index].defvalue.asfloat); + } + result.print(','); + if (isnan(_params[index].minvalue.asfloat)) { + result.print(NAN_PSTR); + } else { + result.printf(FMT_FLOAT_PSTR, _params[index].minvalue.asfloat); + } + result.print(','); + if (isnan(_params[index].maxvalue.asfloat)) { + result.print(NAN_PSTR); + } else { + result.printf(FMT_FLOAT_PSTR, _params[index].maxvalue.asfloat); + } + result.print(");\""); + } + } + } + result.print('>'); + } + } + return result; + } + return String(); +} +#endif + +#ifdef ESP8266 +void Parameters::handleWebPage(ESP8266WebServer &http, const char *restartPath, bool confirmation) { + String page; + + if (http.method() == HTTP_GET) { + http.setContentLength(CONTENT_LENGTH_UNKNOWN); + http.send(200, FPSTR(TEXTHTML_PSTR), FPSTR(EMPTY_PSTR)); + page = F("\n" + "\n" + "\n" + "Parameters\n" + "\n"); + page.concat(getScript()); + page.concat(F("\n" + "\n" + "
\n" + "\n")); + http.sendContent(page); + for (uint16_t i = 0; i < _count; ++i) { + if (pgm_read_byte(&_params[i].editor.type) == editor_t::EDIT_NONE) + continue; + page = F("\n")); + http.sendContent(page); + } + page = F("
"); + if (pgm_read_ptr(&_params[i].title)) + page.concat(encodeString((char*)pgm_read_ptr(&_params[i].title))); + else + page.concat(FPSTR((char*)pgm_read_ptr(&_params[i].name))); + page.concat(F("")); + page.concat(getEditor(i)); + page.concat(F("
\n" + "

\n" + "\n" + "\n")); + if (restartPath) { + page.concat(F("\n")); + } + page.concat(F("

\n" + "\n" + "\n")); + http.sendContent(page); + http.sendContent(FPSTR(EMPTY_PSTR)); + http.client().stop(); + } else if (http.method() == HTTP_POST) { + String errors; + + page = F("\n" + "\n" + "\n" + "Store parameters\n" + "\n" + "\n" + "\n" + "\n")); + for (uint16_t i = 0; i < http.args(); ++i) { + int16_t param = find(http.argName(i).c_str()); + + if (param >= 0) { + if (! fromString(param, http.arg(i))) { + errors.concat(F("Error setting parameter \"")); + errors.concat(http.argName(i)); + errors.concat(F("\"!
\n")); + } + } + } + if (! update()) { + errors.concat(F("Error storing EEPROM parameters!\n")); + } + if (errors.isEmpty()) { + page.concat(F("OK\n")); + } else { + page.concat(F("\n")); + page.concat(errors); + page.concat(F("\n")); + } + page.concat(F("

\n" + "Wait for 5 sec. or click this to return to previous page\n" + "\n" + "\n")); + http.send(errors.isEmpty() ? 200 : 400, FPSTR(TEXTHTML_PSTR), page); + } else if (http.method() == HTTP_DELETE) { + if (clear()) { + http.send(200, FPSTR(TEXTPLAIN_PSTR), F("OK")); + } else { + http.send(400, FPSTR(TEXTPLAIN_PSTR), F("Error clearing EEPROM parameters!")); + } + } +} + +#else +void Parameters::handleWebPage(WebServer &http, const char *restartPath, bool confirmation) { + String page; + + if (http.method() == HTTP_GET) { + http.setContentLength(CONTENT_LENGTH_UNKNOWN); + http.send(200, TEXTHTML_PSTR, EMPTY_PSTR); + page = "\n" + "\n" + "\n" + "Parameters\n" + "\n"; + page.concat(getScript()); + page.concat("\n" + "\n" + "

\n" + "\n"); + http.sendContent(page); + for (uint16_t i = 0; i < _count; ++i) { + if (_params[i].editor.type == editor_t::EDIT_NONE) + continue; + page = "\n"); + http.sendContent(page); + } + page = "
"; + if (_params[i].title) + page.concat(encodeString(_params[i].title)); + else + page.concat(_params[i].name); + page.concat(""); + page.concat(getEditor(i)); + page.concat("
\n" + "

\n" + "\n" + "\n"); + if (restartPath) { + page.concat("\n"); + } + page.concat("

\n" + "\n" + "\n"); + http.sendContent(page); + http.sendContent(EMPTY_PSTR); + http.client().stop(); + } else if (http.method() == HTTP_POST) { + String errors; + + page = "\n" + "\n" + "\n" + "Store parameters\n" + "\n" + "\n" + "\n" + "\n"); + for (uint16_t i = 0; i < http.args(); ++i) { + int16_t param = find(http.argName(i).c_str()); + + if (param >= 0) { + if (! fromString(param, http.arg(i))) { + errors.concat("Error setting parameter \""); + errors.concat(http.argName(i)); + errors.concat("\"!
\n"); + } + } + } + if (! update()) { + errors.concat("Error storing EEPROM parameters!\n"); + } + if (errors.isEmpty()) { + page.concat("OK\n"); + } else { + page.concat("\n"); + page.concat(errors); + page.concat("\n"); + } + page.concat("

\n" + "Wait for 5 sec. or click this to return to previous page\n" + "\n" + "\n"); + http.send(errors.isEmpty() ? 200 : 400, TEXTHTML_PSTR, page); + } else if (http.method() == HTTP_DELETE) { + if (clear()) { + http.send(200, TEXTPLAIN_PSTR, "OK"); + } else { + http.send(400, TEXTPLAIN_PSTR, "Error clearing EEPROM parameters!"); + } + } +} +#endif + +uint16_t Parameters::crc16(uint8_t data, uint16_t crc) { + crc ^= data << 8; + for (uint8_t i = 0; i < 8; ++i) + crc = crc & 0x8000 ? (crc << 1) ^ 0x1021 : crc << 1; + return crc; +} + +uint16_t Parameters::crc16(const uint8_t *data, uint16_t size, uint16_t crc) { + while (size--) { + crc ^= *data++ << 8; + for (uint8_t i = 0; i < 8; ++i) + crc = crc & 0x8000 ? (crc << 1) ^ 0x1021 : crc << 1; + } + return crc; +} + +void *Parameters::getPtr(uint16_t index) const { + void *result = NULL; + + if (_inited && (index < _count)) { + result = EEPROM.getDataPtr() + sizeof(header_t); + for (uint16_t i = 0; i < index; ++i) { +#ifdef ESP8266 + result = result + pgm_read_word(&_params[i].size); +#else + result = result + _params[i].size; +#endif + } + } + return result; +} + +bool Parameters::check() { + if (_inited) { + uint8_t *ptr = EEPROM.getDataPtr(); + const header_t *header = (header_t*)ptr; + + if (header->sign == EEPROM_SIGN) { + uint16_t crc = 0xFFFF; + + ptr += sizeof(header_t); + for (uint16_t i = 0; i < _count; ++i) { +#ifdef ESP8266 + crc = crc16(ptr, pgm_read_word(&_params[i].size), crc); + ptr += pgm_read_word(&_params[i].size); +#else + crc = crc16(ptr, _params[i].size, crc); + ptr += _params[i].size; +#endif + } + return crc == header->crc; + } + } + return false; +} + +bool Parameters::update() { + if (! _inited) + return false; + + uint8_t *ptr = EEPROM.getDataPtr(); + header_t *header = (header_t*)ptr; + uint16_t crc = 0xFFFF; + + ptr += sizeof(header_t); + for (uint16_t i = 0; i < _count; ++i) { +#ifdef ESP8266 + crc = crc16(ptr, pgm_read_word(&_params[i].size), crc); + ptr += pgm_read_word(&_params[i].size); +#else + crc = crc16(ptr, _params[i].size, crc); + ptr += _params[i].size; +#endif + } + if ((header->sign != EEPROM_SIGN) || (header->crc != crc)) { + header->sign = EEPROM_SIGN; + header->crc = crc; + return EEPROM.commit(); + } + return true; +} + +String Parameters::encodeString(char c) { + String result; + + if (c == '"') +#ifdef ESP8266 + result = FPSTR(QUOT_PSTR); +#else + result = QUOT_PSTR; +#endif + else if (c == '<') +#ifdef ESP8266 + result = FPSTR(LT_PSTR); +#else + result = LT_PSTR; +#endif + else if (c == '>') +#ifdef ESP8266 + result = FPSTR(GT_PSTR); +#else + result = GT_PSTR; +#endif + else + result = c; + return result; +} + +String Parameters::encodeString(const char *str) { + String result; + + if (str) { +#ifdef ESP8266 + uint16_t len = strlen_P(str); +#else + uint16_t len = strlen(str); +#endif + + if (result.reserve(len)) { + for (uint16_t i = 0; i < len; ++i) { +#ifdef ESP8266 + char c = pgm_read_byte(&str[i]); + + if (c == '"') + result.concat(FPSTR(QUOT_PSTR)); + else if (c == '<') + result.concat(FPSTR(LT_PSTR)); + else if (c == '>') + result.concat(FPSTR(GT_PSTR)); + else + result.concat(c); +#else + if (str[i] == '"') + result.concat(QUOT_PSTR); + else if (str[i] == '<') + result.concat(LT_PSTR); + else if (str[i] == '>') + result.concat(GT_PSTR); + else + result.concat(str[i]); +#endif + } + } + } + return result; +} + +static uint8_t findFreeChannel() { + uint8_t result = 0; + int16_t networks; + + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + networks = WiFi.scanNetworks(false, true); + if (networks > 0) { + int8_t signals[13]; + + memset(signals, -128, sizeof(signals)); + for (uint8_t i = 0; i < networks; ++i) { + uint8_t channel = WiFi.channel(i); + int8_t rssi = WiFi.RSSI(i); + + if (rssi > signals[channel - 1]) + signals[channel - 1] = rssi; + } + for (uint8_t i = 1; i < 13; ++i) { + if (signals[i] < signals[result]) + result = i; + } + } + WiFi.scanDelete(); + return result + 1; +} + +bool paramsCaptivePortal(Parameters *params, const char *ssid, const char *pswd, uint16_t duration, cpcallback_t callback) { +#ifdef ESP8266 + static const char SLASH_PSTR[] PROGMEM = "/"; + static const char RESTART_PSTR[] PROGMEM = "/restart"; + static const char GENERATE204_PSTR[] PROGMEM = "/generate_204"; +#else + static const char SLASH_PSTR[] = "/"; + static const char RESTART_PSTR[] = "/restart"; + static const char GENERATE204_PSTR[] = "/generate_204"; +#endif + + { + uint8_t channel = findFreeChannel(); + + WiFi.mode(WIFI_AP); +#ifdef ESP8266 + { + char _ssid[strlen_P(ssid) + 1], _pswd[strlen_P(pswd) + 1]; + + strcpy_P(_ssid, ssid); + strcpy_P(_pswd, pswd); + if (! WiFi.softAP(_ssid, _pswd, channel)) + return false; + } +#else + if (! WiFi.softAP(ssid, pswd, channel)) + return false; +#endif + } + if (callback) { + callback(CP_INIT, &WiFi); + } + + DNSServer *dns; +#ifdef ESP8266 + ESP8266WebServer *http; +#else + WebServer *http; +#endif + + dns = new DNSServer(); + if (! dns) { + WiFi.softAPdisconnect(true); + return false; + } + dns->setErrorReplyCode(DNSReplyCode::NoError); +#ifdef ESP8266 + if (! dns->start(53, F("*"), WiFi.softAPIP())) { +#else + if (! dns->start(53, "*", WiFi.softAPIP())) { +#endif + delete dns; + WiFi.softAPdisconnect(true); + return false; + } + +#ifdef ESP8266 + http = new ESP8266WebServer(); +#else + http = new WebServer(); +#endif + if (! http) { + delete dns; + WiFi.softAPdisconnect(true); + return false; + } + http->onNotFound([&]() { + if (! http->hostHeader().equals(WiFi.softAPIP().toString())) { +#ifdef ESP8266 + http->sendHeader(F("Location"), String(F("http://")) + WiFi.softAPIP().toString(), true); + http->send(302, FPSTR(TEXTPLAIN_PSTR), FPSTR(EMPTY_PSTR)); +#else + http->sendHeader("Location", String("http://") + WiFi.softAPIP().toString(), true); + http->send(302, TEXTPLAIN_PSTR, EMPTY_PSTR); +#endif + return; + } + + String page; + +#ifdef ESP8266 + page = F("\n" + "\n" + "\n" + "Page Not Found!\n" + "\n" + "\n" + "\n" + "Page "); + page.concat(http->uri()); + page.concat(F(" not found!\n" + "\n" + "\n")); + http->send(404, FPSTR(TEXTHTML_PSTR), page); +#else + page = "\n" + "\n" + "\n" + "Page Not Found!\n" + "\n" + "\n" + "\n" + "Page "; + page.concat(http->uri()); + page.concat(" not found!\n" + "\n" + "\n"); + http->send(404, TEXTHTML_PSTR, page); +#endif + }); +#ifdef ESP8266 + http->on(FPSTR(SLASH_PSTR), [&]() { + params->handleWebPage(*http, RESTART_PSTR); +#else + http->on(SLASH_PSTR, [&]() { + params->handleWebPage(*http, RESTART_PSTR); +#endif + }); +#ifdef ESP8266 + http->on(FPSTR(GENERATE204_PSTR), [&]() { + params->handleWebPage(*http, RESTART_PSTR, false); +#else + http->on(GENERATE204_PSTR, [&]() { + params->handleWebPage(*http, RESTART_PSTR, false); +#endif + }); +#ifdef ESP8266 + http->on(FPSTR(RESTART_PSTR), HTTP_GET, [&]() { + http->send_P(200, TEXTHTML_PSTR, PSTR("\n" + "\n" + "\n" + "Restart\n" + "\n" + "\n" + "\n" + "\n" + "Restarting...\n" + "\n" + "")); +#else + http->on(RESTART_PSTR, HTTP_GET, [&]() { + http->send(200, TEXTHTML_PSTR, "\n" + "\n" + "\n" + "Restart\n" + "\n" + "\n" + "\n" + "\n" + "Restarting...\n" + "\n" + ""); +#endif + http->client().stop(); + if (callback) { + callback(CP_RESTART, NULL); + } + ESP.restart(); + }); + if (callback) { + callback(CP_WEB, http); + } + http->begin(); + + uint8_t clients = 0; + + if (duration) { + uint32_t start = millis(); + + while (millis() - start < duration * 1000) { + uint8_t cln = WiFi.softAPgetStationNum(); + + if (cln != clients) { + if (cln && (! clients)) { // First client connected + if (callback) { + callback(CP_CONNECT, &WiFi); + } + } else if ((! cln) && clients) { // Last client disconnected + if (callback) { + callback(CP_DISCONNECT, &WiFi); + } + } + clients = cln; + } + if (cln) + start = millis(); + dns->processNextRequest(); + http->handleClient(); + if (callback) { + callback(CP_IDLE, &WiFi); + } + delay(1); + } + } else { + for (;;) { + uint8_t cln = WiFi.softAPgetStationNum(); + + if (cln != clients) { + if (cln && (! clients)) { // First client connected + if (callback) { + callback(CP_CONNECT, &WiFi); + } + } else if ((! cln) && clients) { // Last client disconnected + if (callback) { + callback(CP_DISCONNECT, &WiFi); + } + } + clients = cln; + } + dns->processNextRequest(); + http->handleClient(); + if (callback) { + callback(CP_IDLE, &WiFi); + } + delay(1); + } + } + delete http; + delete dns; + WiFi.softAPdisconnect(); + if (callback) { + callback(CP_DONE, &WiFi); + } + return true; +} diff --git a/src/RtcFlags.cpp b/src/RtcFlags.cpp new file mode 100644 index 0000000..0cc3147 --- /dev/null +++ b/src/RtcFlags.cpp @@ -0,0 +1,31 @@ +#include "RtcFlags.h" + +uint16_t RtcFlags::getFlags() { + uint32_t rtcData[2]; + + if (ESP.rtcUserMemoryRead(0, rtcData, sizeof(rtcData))) { + if ((rtcData[0] == RTC_SIGN) && (((~rtcData[1]) >> 16) == (rtcData[1] & 0xFFFF))) + return rtcData[1] & 0xFFFF; + } + return 0; +} + +bool RtcFlags::getFlag(uint8_t flag) { + return (getFlags() & (1 << flag)) != 0; +} + +bool RtcFlags::setFlags(uint16_t flags) { + uint32_t rtcData[2]; + + rtcData[0] = RTC_SIGN; + rtcData[1] = ((~flags) << 16) | flags; + return ESP.rtcUserMemoryWrite(0, rtcData, sizeof(rtcData)); +} + +bool RtcFlags::setFlag(uint8_t flag) { + return setFlags(getFlags() | (1 << flag)); +} + +bool RtcFlags::clearFlag(uint8_t flag) { + return setFlags(getFlags() & ~(1 << flag)); +} diff --git a/src/StrUtils.cpp b/src/StrUtils.cpp new file mode 100644 index 0000000..7a5ab9f --- /dev/null +++ b/src/StrUtils.cpp @@ -0,0 +1,49 @@ +#include +#include +#include "StrUtils.h" + +String printfToString(const char *fmt, ...) { + const uint16_t MAX_SIZE = 256; + + va_list vargs; + char msg[MAX_SIZE]; + + va_start(vargs, fmt); + vsnprintf_P(msg, sizeof(msg), fmt, vargs); + va_end(vargs); + return String(msg); +} + +int8_t strcmp_PP(const char *s1, const char *s2) { + char c1, c2; + + do { + c1 = pgm_read_byte(s1++); + c2 = pgm_read_byte(s2++); + } while (c1 && (c1 == c2)); + return c1 - c2; +} + +int8_t strncmp_PP(const char *s1, const char *s2, uint16_t maxlen) { + char c1, c2; + + do { + c1 = pgm_read_byte(s1++); + c2 = pgm_read_byte(s2++); + } while (c1 && (c1 == c2) && maxlen--); + return c1 - c2; +} + +int sscanf_P(const char *str, const char *fmt, ...) { + va_list vargs; + int result; + char _str[strlen_P(str) + 1]; + char _fmt[strlen_P(fmt) + 1]; + + strcpy_P(_str, str); + strcpy_P(_fmt, fmt); + va_start(vargs, fmt); + result = vsscanf(_str, _fmt, vargs); + va_end(vargs); + return result; +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c416d1a --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,412 @@ +#include +#include +#include +#include +#include +#include "Parameters.h" +#include "RtcFlags.h" + +const uint8_t RELAY_PIN = 0; +const bool RELAY_LEVEL = HIGH; + +const uint8_t LED_PIN = 2; +const bool LED_LEVEL = LOW; + +const uint32_t LED_PULSE = 25; // 25 ms. + +const char CP_SSID[] PROGMEM = "ESP01_Relay"; +const char CP_PSWD[] PROGMEM = "1029384756"; + +const char PARAM_WIFI_SSID_NAME[] PROGMEM = "wifi_ssid"; +const char PARAM_WIFI_SSID_TITLE[] PROGMEM = "WiFi SSID"; +const char PARAM_WIFI_PSWD_NAME[] PROGMEM = "wifi_pswd"; +const char PARAM_WIFI_PSWD_TITLE[] PROGMEM = "WiFi password"; +const char PARAM_MQTT_SERVER_NAME[] PROGMEM = "mqtt_server"; +const char PARAM_MQTT_SERVER_TITLE[] PROGMEM = "MQTT broker"; +const char PARAM_MQTT_PORT_NAME[] PROGMEM = "mqtt_port"; +const char PARAM_MQTT_PORT_TITLE[] PROGMEM = "MQTT port"; +const uint16_t PARAM_MQTT_PORT_DEF = 1883; +const char PARAM_MQTT_CLIENT_NAME[] PROGMEM = "mqtt_client"; +const char PARAM_MQTT_CLIENT_TITLE[] PROGMEM = "MQTT client"; +const char PARAM_MQTT_CLIENT_DEF[] PROGMEM = "ESP01_Relay"; +const char PARAM_MQTT_USER_NAME[] PROGMEM = "mqtt_user"; +const char PARAM_MQTT_USER_TITLE[] PROGMEM = "MQTT user"; +const char PARAM_MQTT_PSWD_NAME[] PROGMEM = "mqtt_pswd"; +const char PARAM_MQTT_PSWD_TITLE[] PROGMEM = "MQTT password"; +const char PARAM_MQTT_TOPIC_NAME[] PROGMEM = "mqtt_topic"; +const char PARAM_MQTT_TOPIC_TITLE[] PROGMEM = "MQTT topic"; +const char PARAM_MQTT_TOPIC_DEF[] PROGMEM = "/Relay"; +const char PARAM_MQTT_RETAINED_NAME[] PROGMEM = "mqtt_retain"; +const char PARAM_MQTT_RETAINED_TITLE[] PROGMEM = "MQTT retained"; +const bool PARAM_MQTT_RETAINED_DEF = false; +const char PARAM_BOOT_STATE_NAME[] PROGMEM = "boot_state"; +const char PARAM_BOOT_STATE_TITLE[] PROGMEM = "Relay state on boot"; +const bool PARAM_BOOT_STATE_DEF = false; +const char PARAM_PERSISTENT_NAME[] PROGMEM = "persist"; +const char PARAM_PERSISTENT_TITLE[] PROGMEM = "Persistent relay state"; +const bool PARAM_PERSISTENT_DEF = false; + +const char OFF_PSTR[] PROGMEM = "OFF"; +const char ON_PSTR[] PROGMEM = "ON"; +const char *const STATES[] PROGMEM = { OFF_PSTR, ON_PSTR }; + +const paraminfo_t PARAMS[] PROGMEM = { + PARAM_STR(PARAM_WIFI_SSID_NAME, PARAM_WIFI_SSID_TITLE, 33, NULL), + PARAM_PASSWORD(PARAM_WIFI_PSWD_NAME, PARAM_WIFI_PSWD_TITLE, 33, NULL), + PARAM_STR(PARAM_MQTT_SERVER_NAME, PARAM_MQTT_SERVER_TITLE, 33, NULL), + PARAM_U16(PARAM_MQTT_PORT_NAME, PARAM_MQTT_PORT_TITLE, PARAM_MQTT_PORT_DEF), + PARAM_STR(PARAM_MQTT_CLIENT_NAME, PARAM_MQTT_CLIENT_TITLE, 33, PARAM_MQTT_CLIENT_DEF), + PARAM_STR(PARAM_MQTT_USER_NAME, PARAM_MQTT_USER_TITLE, 33, NULL), + PARAM_PASSWORD(PARAM_MQTT_PSWD_NAME, PARAM_MQTT_PSWD_TITLE, 33, NULL), + PARAM_STR(PARAM_MQTT_TOPIC_NAME, PARAM_MQTT_TOPIC_TITLE, 33, PARAM_MQTT_TOPIC_DEF), + PARAM_BOOL(PARAM_MQTT_RETAINED_NAME, PARAM_MQTT_RETAINED_TITLE, PARAM_MQTT_RETAINED_DEF), + PARAM_BOOL_CUSTOM(PARAM_BOOT_STATE_NAME, PARAM_BOOT_STATE_TITLE, PARAM_BOOT_STATE_DEF, EDITOR_RADIO(2, BOOLS, STATES, false, false, false)), + PARAM_BOOL(PARAM_PERSISTENT_NAME, PARAM_PERSISTENT_TITLE, PARAM_PERSISTENT_DEF) +}; + +Parameters *params = NULL; +ESP8266WebServer *http = NULL; +WiFiClient *client = NULL; +PubSubClient *mqtt = NULL; +bool relayState; + +static void halt(const char *msg = NULL) { + if (msg) + Serial.println(FPSTR(msg)); + Serial.flush(); + ESP.deepSleep(0); +} + +static void restart(const char *msg = NULL) { + if (msg) + Serial.println(FPSTR(msg)); + Serial.flush(); + ESP.restart(); +} + +static void relaySwitch(bool on, bool publish = false) { + digitalWrite(RELAY_PIN, on == RELAY_LEVEL); + if (publish && mqtt && mqtt->connected()) { + char value; + + value = '0' + on; + mqtt->publish((char*)params->value(PARAM_MQTT_TOPIC_NAME), (uint8_t*)&value, 1, *(bool*)params->value(PARAM_MQTT_RETAINED_NAME)); + } + relayState = on; + if (*(bool*)params->value(PARAM_PERSISTENT_NAME)) { + params->set(PARAM_BOOT_STATE_NAME, &relayState); + params->update(); + } +} + +static void httpPageNotFound() { + http->send_P(404, PSTR("text/plain"), PSTR("Page Not Found!")); +} + +static void httpRootPage() { + String page; + + page = F("\n" + "\n" + "\n" + "ESP01-Relay\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "

\n" + "\n" + "\n" + "\n" + "")); + http->send(200, F("text/html"), page); +} + +static void httpSwitchPage() { + if (http->method() == HTTP_GET) { + String page = F("{\"state\":"); + + page.concat(FPSTR(BOOLS[relayState])); + page.concat('}'); + http->send(200, F("text/json"), page); + } else if (http->method() == HTTP_POST) { + bool error = true; + + if (http->hasArg(F("on"))) { + String param = http->arg(F("on")); + + if (param.equals(FPSTR(BOOLS[false])) || param.equals(FPSTR(BOOLS[true]))) { + relaySwitch(param.equals(FPSTR(BOOLS[true])), true); + error = false; + } + } + http->send_P(error ? 400 : 200, PSTR("text/plain"), error ? PSTR("Bad argument!") : PSTR("OK")); + } else { + http->send_P(405, PSTR("text/plain"), PSTR("Method Not Allowed!")); + } +} + +static void httpRestartPage() { + http->send_P(200, PSTR("text/html"), PSTR("\n" + "\n" + "\n" + "Restart\n" + "\n" + "\n" + "\n" + "\n" + "Restarting...\n" + "\n" + "")); + http->client().stop(); + restart(PSTR("Restarting...")); +} + +void setup() { + WiFi.persistent(false); + + Serial.begin(115200); + Serial.println(); + + params = new Parameters(PARAMS, ARRAY_SIZE(PARAMS)); + if ((! params) || (! params->begin())) + halt(PSTR("Initialization of parameters FAIL!")); + + relayState = *(bool*)params->value(PARAM_BOOT_STATE_NAME); + + pinMode(RELAY_PIN, OUTPUT); + digitalWrite(RELAY_PIN, relayState == RELAY_LEVEL); + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, ! LED_LEVEL); + + bool paramIncomplete = (! *(char*)params->value(PARAM_WIFI_SSID_NAME)) || (! *(char*)params->value(PARAM_WIFI_PSWD_NAME)); + + if (paramIncomplete || ((ESP.getResetInfoPtr()->reason != REASON_SOFT_RESTART) && (! RtcFlags::getFlag(0)))) { + RtcFlags::setFlag(0); + if (! paramsCaptivePortal(params, CP_SSID, CP_PSWD, paramIncomplete ? 0 : 60, [&](cpevent_t event, void *param) { + switch (event) { + case CP_INIT: + Serial.print(F("Captive portal \"")); + Serial.print(((ESP8266WiFiClass*)param)->softAPSSID()); + Serial.print(F("\" with password \"")); + Serial.print(((ESP8266WiFiClass*)param)->softAPPSK()); + Serial.println(F("\" started")); + Serial.print(F("Visit to http://")); + Serial.println(((ESP8266WiFiClass*)param)->softAPIP()); + break; + case CP_DONE: + digitalWrite(LED_PIN, ! LED_LEVEL); + Serial.println(F("Captive portal closed")); + break; + case CP_RESTART: + digitalWrite(LED_PIN, ! LED_LEVEL); + Serial.println(F("Restarting...")); + Serial.flush(); + break; + case CP_IDLE: + digitalWrite(LED_PIN, (millis() % (((ESP8266WiFiClass*)param)->softAPgetStationNum() > 0 ? 500 : 250) < LED_PULSE) == LED_LEVEL); + break; + default: + break; + } + })) + halt(PSTR("Captive portal FAIL!")); + + relayState = *(bool*)params->value(PARAM_BOOT_STATE_NAME); + digitalWrite(RELAY_PIN, relayState == RELAY_LEVEL); + } + RtcFlags::clearFlag(0); + if ((! *(char*)params->value(PARAM_WIFI_SSID_NAME)) || (! *(char*)params->value(PARAM_WIFI_PSWD_NAME))) + restart(PSTR("Parameters incomplete!")); + + http = new ESP8266WebServer(); + if (! http) + halt(PSTR("Web server initialization FAIL!")); + http->onNotFound(httpPageNotFound); + http->on(F("/"), HTTP_GET, httpRootPage); + http->on(F("/index.html"), HTTP_GET, httpRootPage); + http->on(F("/switch"), httpSwitchPage); + http->on(F("/setup"), [&]() { + params->handleWebPage(*http); + }); + http->on(F("/restart"), HTTP_GET, httpRestartPage); + http->on(F("/description.xml"), HTTP_GET, [&]() { + SSDP.schema(http->client()); + }); + + SSDP.setSchemaURL(F("description.xml")); + SSDP.setHTTPPort(80); + SSDP.setName(F("ESP01 Relay")); + SSDP.setSerialNumber(F("12345678")); + SSDP.setURL(F("index.html")); + SSDP.setModelName(F("ESP01S Relay")); + SSDP.setModelNumber(F("v1.0")); + SSDP.setManufacturer(F("NoName Ltd.")); + SSDP.setDeviceType(F("upnp:rootdevice")); + + if (*(char*)params->value(PARAM_MQTT_SERVER_NAME) && *(char*)params->value(PARAM_MQTT_CLIENT_NAME) && *(char*)params->value(PARAM_MQTT_TOPIC_NAME)) { + client = new WiFiClient(); + if (! client) + halt(PSTR("WiFi client creation FAIL!")); + mqtt = new PubSubClient(*client); + if (! mqtt) + halt(PSTR("MQTT initialization FAIL!")); + mqtt->setServer((char*)params->value(PARAM_MQTT_SERVER_NAME), *(uint16_t*)params->value(PARAM_MQTT_PORT_NAME)); + mqtt->setCallback([&](char *topic, uint8_t *payload, unsigned int length) { + if (! strcmp(topic, (char*)params->value(PARAM_MQTT_TOPIC_NAME))) { + if ((length == 1) && (*payload >= '0') && (*payload <= '1')) { + relaySwitch(*payload - '0'); + } + } + }); + } + + WiFi.mode(WIFI_STA); + { + const char *name = (char*)params->value(PARAM_MQTT_CLIENT_NAME); + + if (*name) { + if (! WiFi.hostname(name)) { + Serial.println(F("Error setting host name!")); + } + } + } + + Serial.println(F("Relay started")); +} + +void loop() { + const uint32_t WIFI_TIMEOUT = 30000; // 30 sec. + const uint32_t MQTT_TIMEOUT = 30000; // 30 sec. + + static uint32_t lastWiFiTry = 0; + static uint32_t lastMqttTry = 0; + + if (! WiFi.isConnected()) { + if ((! lastWiFiTry) || (millis() - lastWiFiTry >= WIFI_TIMEOUT)) { + uint32_t start; + + SSDP.end(); + { + const char *ssid; + + ssid = (char*)params->value(PARAM_WIFI_SSID_NAME); + WiFi.begin(ssid, (char*)params->value(PARAM_WIFI_PSWD_NAME)); + Serial.print(F("Connecting to SSID \"")); + Serial.print(ssid); + Serial.print('"'); + } + start = millis(); + while ((! WiFi.isConnected()) && (millis() - start < WIFI_TIMEOUT)) { + digitalWrite(LED_PIN, LED_LEVEL); + delay(LED_PULSE); + digitalWrite(LED_PIN, ! LED_LEVEL); + Serial.print('.'); + delay(500 - LED_PULSE); + } + if (WiFi.isConnected()) { + http->begin(); + SSDP.begin(); + Serial.print(F(" OK (")); + Serial.print(WiFi.localIP()); + Serial.println(')'); + lastWiFiTry = 0; + } else { + WiFi.disconnect(); + Serial.println(F(" FAIL!")); + lastWiFiTry = millis(); + } + } + } else { + http->handleClient(); + if (mqtt) { + if (! mqtt->connected()) { + if ((! lastMqttTry) || (millis() - lastMqttTry >= MQTT_TIMEOUT)) { + const char *user, *pswd; + bool connected; + + user = (char*)params->value(PARAM_MQTT_USER_NAME); + pswd = (char*)params->value(PARAM_MQTT_PSWD_NAME); + digitalWrite(LED_PIN, LED_LEVEL); + Serial.print(F("Connecting to MQTT broker \"")); + Serial.print((char*)params->value(PARAM_MQTT_SERVER_NAME)); + Serial.print(F("\"... ")); + if (*user && *pswd) + connected = mqtt->connect((char*)params->value(PARAM_MQTT_CLIENT_NAME), user, pswd); + else + connected = mqtt->connect((char*)params->value(PARAM_MQTT_CLIENT_NAME)); + digitalWrite(LED_PIN, ! LED_LEVEL); + if (connected) { + Serial.println(F("OK")); + mqtt->subscribe((char*)params->value(PARAM_MQTT_TOPIC_NAME)); + lastMqttTry = 0; + } else { + Serial.println(F("FAIL!")); + lastMqttTry = millis(); + } + } + } else { + mqtt->loop(); + } + } + digitalWrite(LED_PIN, (millis() % 1000 < LED_PULSE) == LED_LEVEL); + } +// delay(1); +}