From aeb161455ec79ebe5608937e8b66e91a70d35ecf Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Wed, 15 Jan 2020 20:17:06 +0100 Subject: [PATCH 1/2] Enable config page on runtime with populated form --- lib/HanConfigAp/src/HanConfigAp.cpp | 107 +++++++++++++++++----- lib/HanConfigAp/src/HanConfigAp.h | 2 + lib/HanConfigAp/src/config_html.h | 72 +++++++++++++++ lib/HanConfigAp/src/style_css.h | 135 ++++++++++++++++++++++++++++ src/AmsToMqttBridge.ino | 11 ++- 5 files changed, 303 insertions(+), 24 deletions(-) create mode 100644 lib/HanConfigAp/src/config_html.h create mode 100644 lib/HanConfigAp/src/style_css.h diff --git a/lib/HanConfigAp/src/HanConfigAp.cpp b/lib/HanConfigAp/src/HanConfigAp.cpp index 06a6c0fc..43933e0a 100644 --- a/lib/HanConfigAp/src/HanConfigAp.cpp +++ b/lib/HanConfigAp/src/HanConfigAp.cpp @@ -1,4 +1,6 @@ #include "HanConfigAp.h" +#include "config_html.h" +#include "style_css.h" #if defined(ESP8266) ESP8266WebServer HanConfigAp::server(80); @@ -66,30 +68,34 @@ void HanConfigAp::setup(int accessPointButtonPin, Stream* debugger) /* Setup the DNS server redirecting all the domains to this IP */ dnsServer.setErrorReplyCode(DNSReplyCode::NoError); dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); - - server.on("/", handleRoot); - server.on("/save", handleSave); - server.begin(); // Web server start - - print("Web server is ready for config at http://"); - print(WiFi.softAPIP()); - println("/"); } } +void HanConfigAp::enableWeb() { + server.on("/", handleRoot); + server.on("/style.css", handleStyle); + server.on("/save", handleSave); + server.begin(); // Web server start + + print("Web server is ready for config at http://"); + if(isActivated) { + print(WiFi.softAPIP()); + } else { + print(WiFi.localIP()); + } + println("/"); +} + bool HanConfigAp::loop() { - if (isActivated) - { + if(isActivated) { //DNS dnsServer.processNextRequest(); - //HTTP - server.handleClient(); - return true; - } - else - { - return false; } + + //HTTP + server.handleClient(); + + return isActivated; } /** Handle root or redirect to captive portal */ @@ -99,11 +105,72 @@ void HanConfigAp::handleRoot() { server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); - server.setContentLength(CONTENT_LENGTH_UNKNOWN); - String html = String("\r\n\r\n\r\n\t\r\n\r\n\r\n\r\n\t
\r\n\r\n\t\t
\r\n\r\n\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t

WiFi

\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t
\r\n\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t

Meter Type

\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t
\r\n\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t

MQTT

\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t\t
\r\n\t\t\t\t\t\r\n\t\t\t\t
\r\n\t\t\t
\r\n\t\t
\r\n\t
\r\n\r\n\t\r\n\r\n"); + String html = CONFIG_HTML; + + configuration *config = new configuration(); + config->load(); + + if(config->hasConfig()) { + html.replace("${config.ssid}", config->ssid); + html.replace("${config.ssidPassword}", config->ssidPassword); + switch (config->meterType) { + case 1: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", "selected"); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", ""); + break; + case 2: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", "selected"); + html.replace("${config.meterType3}", ""); + break; + case 3: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", "selected"); + break; + default: + html.replace("${config.meterType0}", "selected"); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", ""); + } + html.replace("${config.mqtt}", config->mqtt); + html.replace("${config.mqttPort}", String(config->mqttPort)); + html.replace("${config.mqttClientID}", config->mqttClientID); + html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic); + html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic); + html.replace("${config.mqttUser}", config->mqttUser); + html.replace("${config.mqttPass}", config->mqttPass); + } else { + html.replace("${config.ssid}", ""); + html.replace("${config.ssidPassword}", ""); + html.replace("${config.meterType0}", "selected"); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", ""); + html.replace("${config.mqtt}", ""); + html.replace("${config.mqttPort}", "1883"); + html.replace("${config.mqttClientID}", ""); + html.replace("${config.mqttPublishTopic}", ""); + html.replace("${config.mqttSubscribeTopic}", ""); + html.replace("${config.mqttUser}", ""); + html.replace("${config.mqttPass}", ""); + } server.send(200, "text/html", html); } +void HanConfigAp::handleStyle() { + println("Serving /style.css over http..."); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "text/html", STYLE_CSS, sizeof(STYLE_CSS)); +} void HanConfigAp::handleSave() { configuration *config = new configuration(); @@ -151,7 +218,7 @@ void HanConfigAp::handleSave() { if (HanConfigAp::debugger) config->print(HanConfigAp::debugger); if (config->save()) { - println("Successfully saved. Will roboot now."); + println("Successfully saved. Will reboot now."); String html = "

Successfully Saved!

Device is restarting now...

"; server.send(200, "text/html", html); #if defined(ESP8266) diff --git a/lib/HanConfigAp/src/HanConfigAp.h b/lib/HanConfigAp/src/HanConfigAp.h index 42eab7c6..d1e01d85 100644 --- a/lib/HanConfigAp/src/HanConfigAp.h +++ b/lib/HanConfigAp/src/HanConfigAp.h @@ -27,6 +27,7 @@ class HanConfigAp { public: void setup(int accessPointButtonPin, Stream* debugger); + void enableWeb(); bool loop(); bool hasConfig(); configuration config; @@ -46,6 +47,7 @@ private: // Web server static void handleRoot(); + static void handleStyle(); static void handleSave(); #if defined(ESP8266) static ESP8266WebServer server; diff --git a/lib/HanConfigAp/src/config_html.h b/lib/HanConfigAp/src/config_html.h new file mode 100644 index 00000000..ff2ea9a7 --- /dev/null +++ b/lib/HanConfigAp/src/config_html.h @@ -0,0 +1,72 @@ +const char CONFIG_HTML[] PROGMEM = R"=="==( + + + + + + + + + AMS2MQTT - configuration + + +
+
+
+
+

WiFi

+
+
+ +
+
+ +
+
+
+
+

Meter Type

+
+
+ +
+
+
+
+

MQTT

+
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +)=="=="; diff --git a/lib/HanConfigAp/src/style_css.h b/lib/HanConfigAp/src/style_css.h new file mode 100644 index 00000000..9b28b09c --- /dev/null +++ b/lib/HanConfigAp/src/style_css.h @@ -0,0 +1,135 @@ +const char STYLE_CSS[] PROGMEM = R"=="==( +body,div,input { + font-family: "Roboto", Arial, Lucida Grande; +} +.wrapper { + width: 500px; + position: absolute; + padding: 30px; + background-color: #FFF; + border-radius: 1px; + color: #333; + border-color: rgba(0, 0, 0, 0.03); + box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12); + margin-left: 20px; + margin-top: 20px; +} +div { + padding-bottom: 5px; +} +label { + font-family: "Roboto", "Helvetica Neue", sans-serif; + font-size: 14px; + line-height: 16px; + width: 100px; + display: inline-block; +} +input { + font-family: "Roboto", "Helvetica Neue", sans-serif; + font-size: 14px; + line-height: 16px; + bottom: 30px; + border: none; + border-bottom: 1px solid #d4d4d4; + padding: 10px; + background: transparent; + transition: all .25s ease; +} +input[type=number] { + width: 70px; + margin-left: 5px; +} +input:focus { + outline: none; + border-bottom: 1px solid #3f51b5; +} +h2 { + text-align: left; + font-size: 20px; + font-weight: bold; + letter-spacing: 3px; + line-height: 28px; +} +.submit-button { + position: absolute; + text-align: right; + border-radius: 20px; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + background-color: #3f51b5; + color: #FFF; + padding: 12px 25px; + display: inline-block; + font-size: 12px; + font-weight: bold; + letter-spacing: 2px; + right: 0px; + bottom: 10px; + cursor: pointer; + transition: all .25s ease; + box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12); + width: 100px; +} +.select-style { + border-top: 10px solid white; + border-bottom: 1px solid #d4d4d4; + color: #ffffff; + cursor: pointer; + display: block; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 14px; + font-weight: 400; + height: 16px; + line-height: 14px; + min-width: 200px; + padding-bottom: 7px; + padding-left: 0px; + padding-right: 0px; + position: relative; + text-align: left; + width: 80%; + -webkit-box-direction: normal; + overflow: hidden; + background: #ffffff url("data:image/png;base64,R0lGODlhDwAUAIABAAAAAP///yH5BAEAAAEALAAAAAAPABQAAAIXjI+py+0Po5wH2HsXzmw//lHiSJZmUAAAOw==") no-repeat 98% 50%; +} +.disabled-option { + color: #d4d4d4; +} +.select-style select { + padding: 5px 8px; + width: 100%; + border: none; + box-shadow: none; + background: transparent; + background-image: none; + -webkit-appearance: none; +} +.select-style select:focus { + outline: none; + border: none; +} +@media only screen and (max-width: 1000px) { + .wrapper { + width: 80%; + } +} +@media only screen and (max-width: 300px) { + .wrapper { + width: 75%; + } +} +@media only screen and (max-width: 600px) { + .wrapper { + width: 80%; + margin-left: 0px; + margin-top: 0px; + } + .submit-button { + bottom: 0px; + width: 70px; + } + input { + width: 100%; + } +} +)=="=="; diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 79cec03c..b3279749 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -59,10 +59,11 @@ HardwareSerial* debugger = NULL; HanReader hanReader; // the setup function runs once when you press reset or power the board -void setup() -{ - // Uncomment to debug over the same port as used for HAN communication - //debugger = &Serial; +void setup() { + +#if DEBUG_MODE + debugger = &Serial; +#endif if (debugger) { // Setup serial port for debugging @@ -100,6 +101,8 @@ void setup() // Compensate for the known Kaifa bug hanReader.compensateFor09HeaderBug = (ap.config.meterType == 1); } + + ap.enableWeb(); } // the loop function runs over and over again until power down or reset From 8f0932f1f1c1f85151b4c7deb02172588ca55112 Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Wed, 15 Jan 2020 21:22:06 +0100 Subject: [PATCH 2/2] Added configurable basic auth to web server --- lib/HanConfigAp/src/HanConfigAp.cpp | 119 ++++++++++++++++++-------- lib/HanConfigAp/src/config_html.h | 13 +++ lib/HanConfigAp/src/configuration.cpp | 72 ++++++++++------ lib/HanConfigAp/src/configuration.h | 6 +- platformio.ini | 2 +- 5 files changed, 145 insertions(+), 67 deletions(-) diff --git a/lib/HanConfigAp/src/HanConfigAp.cpp b/lib/HanConfigAp/src/HanConfigAp.cpp index 43933e0a..1c901e49 100644 --- a/lib/HanConfigAp/src/HanConfigAp.cpp +++ b/lib/HanConfigAp/src/HanConfigAp.cpp @@ -2,6 +2,8 @@ #include "config_html.h" #include "style_css.h" +#include "Base64.h" + #if defined(ESP8266) ESP8266WebServer HanConfigAp::server(80); #elif defined(ESP32) // ARDUINO_ARCH_ESP32 @@ -102,50 +104,81 @@ bool HanConfigAp::loop() { void HanConfigAp::handleRoot() { println("Serving / over http..."); - server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - String html = CONFIG_HTML; - configuration *config = new configuration(); config->load(); + String html = CONFIG_HTML; + if(config->hasConfig()) { - html.replace("${config.ssid}", config->ssid); - html.replace("${config.ssidPassword}", config->ssidPassword); - switch (config->meterType) { - case 1: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", "selected"); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", ""); - break; - case 2: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", "selected"); - html.replace("${config.meterType3}", ""); - break; - case 3: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", "selected"); - break; - default: - html.replace("${config.meterType0}", "selected"); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", ""); + bool access = !config->isAuth(); + if(config->isAuth() && server.hasHeader("Authorization")) { + String expectedAuth = String(config->authUser) + ":" + String(config->authPass); + + String providedPwd = server.header("Authorization"); + providedPwd.replace("Basic ", ""); + char inputString[providedPwd.length()]; + providedPwd.toCharArray(inputString, providedPwd.length()+1); + + int inputStringLength = sizeof(inputString); + int decodedLength = Base64.decodedLength(inputString, inputStringLength); + char decodedString[decodedLength]; + Base64.decode(decodedString, inputString, inputStringLength); + print("Received auth: "); + println(decodedString); + access = String(decodedString).equals(expectedAuth); } - html.replace("${config.mqtt}", config->mqtt); - html.replace("${config.mqttPort}", String(config->mqttPort)); - html.replace("${config.mqttClientID}", config->mqttClientID); - html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic); - html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic); - html.replace("${config.mqttUser}", config->mqttUser); - html.replace("${config.mqttPass}", config->mqttPass); + + if(!access) { + server.sendHeader("WWW-Authenticate", "Basic realm=\"Secure Area\""); + server.send(401, "text/html", ""); + } else { + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + + html.replace("${config.ssid}", config->ssid); + html.replace("${config.ssidPassword}", config->ssidPassword); + switch (config->meterType) { + case 1: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", "selected"); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", ""); + break; + case 2: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", "selected"); + html.replace("${config.meterType3}", ""); + break; + case 3: + html.replace("${config.meterType0}", ""); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", "selected"); + break; + default: + html.replace("${config.meterType0}", "selected"); + html.replace("${config.meterType1}", ""); + html.replace("${config.meterType2}", ""); + html.replace("${config.meterType3}", ""); + } + html.replace("${config.mqtt}", config->mqtt); + html.replace("${config.mqttPort}", String(config->mqttPort)); + html.replace("${config.mqttClientID}", config->mqttClientID); + html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic); + html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic); + html.replace("${config.mqttUser}", config->mqttUser); + html.replace("${config.mqttPass}", config->mqttPass); + html.replace("${config.authUser}", config->authUser); + html.replace("${config.authPass}", config->authPass); + } + } else { + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + html.replace("${config.ssid}", ""); html.replace("${config.ssidPassword}", ""); html.replace("${config.meterType0}", "selected"); @@ -159,6 +192,8 @@ void HanConfigAp::handleRoot() { html.replace("${config.mqttSubscribeTopic}", ""); html.replace("${config.mqttUser}", ""); html.replace("${config.mqttPass}", ""); + html.replace("${config.authUser}", ""); + html.replace("${config.authPass}", ""); } server.send(200, "text/html", html); } @@ -213,6 +248,14 @@ void HanConfigAp::handleSave() { config->mqttPass = new char[temp.length() + 1]; temp.toCharArray(config->mqttPass, temp.length() + 1, 0); + temp = server.arg("authUser"); + config->authUser = new char[temp.length() + 1]; + temp.toCharArray(config->authUser, temp.length() + 1, 0); + + temp = server.arg("authPass"); + config->authPass = new char[temp.length() + 1]; + temp.toCharArray(config->authPass, temp.length() + 1, 0); + println("Saving configuration now..."); if (HanConfigAp::debugger) config->print(HanConfigAp::debugger); diff --git a/lib/HanConfigAp/src/config_html.h b/lib/HanConfigAp/src/config_html.h index ff2ea9a7..310b0bce 100644 --- a/lib/HanConfigAp/src/config_html.h +++ b/lib/HanConfigAp/src/config_html.h @@ -65,6 +65,19 @@ const char CONFIG_HTML[] PROGMEM = R"=="==( +
+
+

Webserver

+
+
+ + +
+
+ + +
+
diff --git a/lib/HanConfigAp/src/configuration.cpp b/lib/HanConfigAp/src/configuration.cpp index 94031f07..3fd2e8ce 100644 --- a/lib/HanConfigAp/src/configuration.cpp +++ b/lib/HanConfigAp/src/configuration.cpp @@ -38,6 +38,13 @@ bool configuration::save() else address += saveBool(address, false); + + address += saveBool(address, isAuth()); + if (isAuth()) { + address += saveString(address, authUser); + address += saveString(address, authPass); + } + bool success = EEPROM.commit(); EEPROM.end(); @@ -50,8 +57,22 @@ bool configuration::load() int address = EEPROM_CONFIG_ADDRESS; bool success = false; + ssid = (char*)String("").c_str(); + ssidPassword = (char*)String("").c_str(); + meterType = (byte)0; + mqtt = (char*)String("").c_str(); + mqttClientID = (char*)String("").c_str(); + mqttPublishTopic = (char*)String("").c_str(); + mqttSubscribeTopic = (char*)String("").c_str(); + mqttUser = 0; + mqttPass = 0; + mqttPort = 1883; + authUser = 0; + authPass = 0; + EEPROM.begin(EEPROM_SIZE); - if (EEPROM.read(address) == EEPROM_CHECK_SUM) + int cs = EEPROM.read(address); + if (cs >= 71) { address++; @@ -80,18 +101,18 @@ bool configuration::load() success = true; } - else - { - ssid = (char*)String("").c_str(); - ssidPassword = (char*)String("").c_str(); - meterType = (byte)0; - mqtt = (char*)String("").c_str(); - mqttClientID = (char*)String("").c_str(); - mqttPublishTopic = (char*)String("").c_str(); - mqttSubscribeTopic = (char*)String("").c_str(); - mqttUser = 0; - mqttPass = 0; - mqttPort = 1883; + if(cs >= 72) { + bool auth = false; + address += readBool(address, &auth); + if (auth) { + address += readString(address, &authUser); + address += readString(address, &authPass); + } else { + authUser = 0; + authPass = 0; + } + + success = true; } EEPROM.end(); return success; @@ -102,6 +123,10 @@ bool configuration::isSecure() return (mqttUser != 0) && (String(mqttUser).length() > 0); } +bool configuration::isAuth() { + return (authUser != 0) && (String(authUser).length() > 0); +} + int configuration::readInt(int address, int *value) { int lower = EEPROM.read(address); @@ -147,20 +172,6 @@ int configuration::saveByte(int address, byte value) } void configuration::print(Stream* debugger) { - /* - char* ssid; - char* ssidPassword; - byte meterType; - char* mqtt; - int mqttPort; - char* mqttClientID; - char* mqttPublishTopic; - char* mqttSubscribeTopic; - bool secure; - char* mqttUser; - char* mqttPass; - */ - debugger->println("Configuration:"); debugger->println("-----------------------------------------------"); debugger->printf("ssid: %s\r\n", this->ssid); @@ -178,6 +189,13 @@ void configuration::print(Stream* debugger) debugger->printf("mqttUser: %s\r\n", this->mqttUser); debugger->printf("mqttPass: %s\r\n", this->mqttPass); } + + if (this->isAuth()) { + debugger->printf("WEB AUTH:\r\n"); + debugger->printf("authUser: %s\r\n", this->authUser); + debugger->printf("authPass: %s\r\n", this->authPass); + } + debugger->println("-----------------------------------------------"); } diff --git a/lib/HanConfigAp/src/configuration.h b/lib/HanConfigAp/src/configuration.h index 0dfc86ad..d785a094 100644 --- a/lib/HanConfigAp/src/configuration.h +++ b/lib/HanConfigAp/src/configuration.h @@ -25,8 +25,12 @@ public: char* mqttPass; byte meterType; + char* authUser; + char* authPass; + bool hasConfig(); bool isSecure(); + bool isAuth(); bool save(); bool load(); @@ -35,7 +39,7 @@ protected: private: const int EEPROM_SIZE = 512; - const byte EEPROM_CHECK_SUM = 71; // Used to check if config is stored. Change if structure changes + const byte EEPROM_CHECK_SUM = 72; // Used to check if config is stored. Change if structure changes const int EEPROM_CONFIG_ADDRESS = 0; int saveString(int pAddress, char* pString); diff --git a/platformio.ini b/platformio.ini index 091c3d93..84250766 100755 --- a/platformio.ini +++ b/platformio.ini @@ -3,7 +3,7 @@ extra_configs = platformio-user.ini [common] framework = arduino -lib_deps = HanConfigAp@1.0.0, HanReader@1.0.0, HanToJson@1.0.0, ArduinoJson@^6.0.0, MQTT@^2.4.0, DallasTemperature@^3.8.0 +lib_deps = HanConfigAp@1.0.0, HanReader@1.0.0, HanToJson@1.0.0, ArduinoJson@^6.0.0, MQTT@^2.4.0, DallasTemperature@^3.8.0, Base64@0.0.1 [env:esp12e] platform = espressif8266