diff --git a/main/User_config.h b/main/User_config.h index d899592..8f2e4f6 100644 --- a/main/User_config.h +++ b/main/User_config.h @@ -664,6 +664,9 @@ CRGB leds2[FASTLED_IND_NUM_LEDS]; # endif #endif +/*-----------PLACEHOLDERS FOR WebUI DISPLAY--------------*/ +#define pubWebUI(...) // display the published message onto the OLED display + /*-----------PLACEHOLDERS FOR OLED/LCD DISPLAY--------------*/ // The real definitions are in config_M5.h / config_SSD1306.h #define pubOled(...) // display the published message onto the OLED display diff --git a/main/ZdisplaySSD1306.ino b/main/ZdisplaySSD1306.ino index b49e18b..e0198ba 100644 --- a/main/ZdisplaySSD1306.ino +++ b/main/ZdisplaySSD1306.ino @@ -40,10 +40,9 @@ # include "config_SSD1306.h" SemaphoreHandle_t semaphoreOLEDOperation; -QueueHandle_t displayQueue; + boolean logToOLEDDisplay = LOG_TO_OLED; boolean jsonDisplay = JSON_TO_OLED; -boolean displayMetric = DISPLAY_METRIC; boolean displayFlip = DISPLAY_FLIP; boolean displayState = DISPLAY_STATE; boolean idlelogo = DISPLAY_IDLE_LOGO; @@ -61,17 +60,15 @@ void logToOLED(bool display) { module setup, for use in Arduino setup */ void setupSSD1306() { + SSD1306Config_init(); + SSD1306Config_load(); Log.trace(F("Setup SSD1306 Display" CR)); Log.trace(F("ZdisplaySSD1306 command topic: %s" CR), subjectMQTTtoSSD1306set); Log.trace(F("ZdisplaySSD1306 log-oled: %T" CR), logToOLEDDisplay); Log.trace(F("ZdisplaySSD1306 json-oled: %T" CR), jsonDisplay); Log.trace(F("ZdisplaySSD1306 DISPLAY_PAGE_INTERVAL: %d" CR), DISPLAY_PAGE_INTERVAL); - Log.trace(F("ZdisplaySSD1306 DISPLAY_IDLE_LOGO: %T" CR), DISPLAY_IDLE_LOGO); - Log.trace(F("ZdisplaySSD1306 DISPLAY_METRIC: %T" CR), displayMetric); + Log.trace(F("ZdisplaySSD1306 DISPLAY_IDLE_LOGO: %T" CR), idlelogo); Log.trace(F("ZdisplaySSD1306 DISPLAY_FLIP: %T" CR), displayFlip); - - SSD1306Config_init(); - SSD1306Config_load(); Oled.begin(); Log.notice(F("Setup SSD1306 Display end" CR)); @@ -96,16 +93,10 @@ void loopSSD1306() { long enough since the last message and display not being used and a queue message waiting */ if (jsonDisplay && displayState) { - if (uptime() >= nextDisplayPage && uxSemaphoreGetCount(semaphoreOLEDOperation) && uxQueueMessagesWaiting(displayQueue)) { - displayQueueMessage* message = nullptr; - xQueueReceive(displayQueue, &message, portMAX_DELAY); - if (!Oled.displayPage(message)) { - Log.warning(F("[ssd1306] displayPage failed: %s" CR), message->title); + if (uptime() >= nextDisplayPage && uxSemaphoreGetCount(semaphoreOLEDOperation) && currentWebUIMessage) { + if (!Oled.displayPage(currentWebUIMessage)) { + Log.warning(F("[ssd1306] displayPage failed: %s" CR), currentWebUIMessage->title); } - if (currentOledMessage) { - free(currentOledMessage); - } - currentOledMessage = message; nextDisplayPage = uptime() + DISPLAY_PAGE_INTERVAL; logoDisplayed = false; } @@ -158,11 +149,6 @@ void MQTTtoSSD1306(char* topicOri, JsonObject& SSD1306data) { // json object dec Log.notice(F("Set json-oled: %T" CR), jsonDisplay); success = true; } - if (SSD1306data.containsKey("displaymetric")) { - displayMetric = SSD1306data["displaymetric"].as(); - Log.notice(F("Set displaymetric: %T" CR), displayMetric); - success = true; - } if (SSD1306data.containsKey("idlelogo")) { idlelogo = SSD1306data["idlelogo"].as(); success = true; @@ -212,7 +198,6 @@ void SSD1306Config_save() { jo["brightness"] = displayBrightness; jo["log-oled"] = logToOLEDDisplay; jo["json-oled"] = jsonDisplay; - jo["displaymetric"] = displayMetric; jo["idlelogo"] = idlelogo; jo["display-flip"] = displayFlip; // Save config into NVS (non-volatile storage) @@ -229,7 +214,6 @@ void SSD1306Config_init() { displayBrightness = DISPLAY_BRIGHTNESS; logToOLEDDisplay = LOG_TO_OLED; jsonDisplay = JSON_TO_OLED; - displayMetric = DISPLAY_METRIC; idlelogo = DISPLAY_IDLE_LOGO; displayFlip = DISPLAY_FLIP; Log.notice(F("SSD1306 config initialised" CR)); @@ -255,7 +239,6 @@ bool SSD1306Config_load() { displayBrightness = jo["brightness"].as(); logToOLEDDisplay = jo["log-oled"].as(); jsonDisplay = jo["json-oled"].as(); - displayMetric = jo["displaymetric"].as(); idlelogo = jo["idlelogo"].as(); displayFlip = jo["display-flip"].as(); Log.notice(F("Saved SSD1306 config loaded" CR)); @@ -267,531 +250,6 @@ bool SSD1306Config_load() { } } -/* -Workaround for c not having a string based switch/case function -*/ -constexpr unsigned int hash(const char* s, int off = 0) { // workaround for switching on a string https://stackoverflow.com/a/46711735/18643696 - return !s[off] ? 5381 : (hash(s, off + 1) * 33) ^ s[off]; -} - -/* -Parse json message from module into a format for displaying on screen, and queue for display -*/ -void ssd1306PubPrint(const char* topicori, JsonObject& data) { - if (jsonDisplay && displayState) { - displayQueueMessage* message = (displayQueueMessage*)malloc(sizeof(displayQueueMessage)); - if (message != NULL) { - char* topic = strdup(topicori); - strlcpy(message->title, strtok(topic, "/"), OLED_TEXT_WIDTH); - free(topic); - - Oled.display->normalDisplay(); - - switch (hash(message->title)) { - case hash("SYStoMQTT"): { - // {"uptime":456356,"version":"lilygo-rtl_433-test-A-v1.1.1-25-g574177d[lily-cloud]","freemem":125488,"mqttport":"1883","mqttsecure":false,"freestack":3752,"rssi":-36,"SSID":"The_Beach","BSSID":"64:A5:C3:69:C3:38","ip":"192.168.1.239","mac":"4C:75:25:A8:D5:D8","actRec":3,"mhz":433.92,"RTLRssiThresh":-98,"RTLRssi":-108,"RTLAVGRssi":-107,"RTLCnt":121707,"RTLOOKThresh":90,"modules":["LILYGO_OLED","CLOUD","rtl_433"]} - - // Line 1 - - strlcpy(message->line1, data["version"], OLED_TEXT_WIDTH); - - // Line 2 - - String uptime = data["uptime"]; - String line2 = "uptime: " + uptime; - line2.toCharArray(message->line2, OLED_TEXT_WIDTH); - - // Line 3 - - String freemem = data["freemem"]; - String line3 = "freemem: " + freemem; - line3.toCharArray(message->line3, OLED_TEXT_WIDTH); - - // Line 4 - - String ip = data["ip"]; - String line4 = "ip: " + ip; - line4.toCharArray(message->line4, OLED_TEXT_WIDTH); - - // Queue completed message - - if (xQueueSend(displayQueue, (void*)&message, 0) != pdTRUE) { - Log.error(F("[ SSD1306 ] ERROR: displayQueue full, discarding signal %s" CR), message->title); - } else { - // Log.notice(F("Queued %s" CR), message->title); - } - break; - } - -# ifdef ZgatewayRTL_433 - case hash("RTL_433toMQTT"): { - // {"model":"Acurite-Tower","id":2043,"channel":"B","battery_ok":1,"temperature_C":5.3,"humidity":81,"mic":"CHECKSUM","protocol":"Acurite 592TXR Temp/Humidity, 5n1 Weather Station, 6045 Lightning, 3N1, Atlas","rssi":-81,"duration":121060} - - // Line 1 - - strlcpy(message->line1, data["model"], OLED_TEXT_WIDTH); - - // Line 2 - - String id = data["id"]; - String channel = data["channel"]; - String line2 = "id: " + id + " channel: " + channel; - line2.toCharArray(message->line2, OLED_TEXT_WIDTH); - - // Line 3 - - String line3 = ""; - - if (data.containsKey("temperature_C")) { - float temperature_C = data["temperature_C"]; - char temp[5]; - - if (displayMetric) { - dtostrf(temperature_C, 3, 1, temp); - line3 = "temp: " + (String)temp + "°C "; - } else { - dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp); - line3 = "temp: " + (String)temp + "°F "; - } - } - - float humidity = data["humidity"]; - if (data.containsKey("humidity") && humidity <= 100 && humidity >= 0) { - char hum[5]; - dtostrf(humidity, 3, 1, hum); - line3 += "hum: " + (String)hum + "% "; - } - if (data.containsKey("wind_avg_km_h")) { - float wind_avg_km_h = data["wind_avg_km_h"]; - char wind[6]; - - if (displayMetric) { - dtostrf(wind_avg_km_h, 3, 1, wind); - line3 += "wind: " + (String)wind + "km/h "; - } else { - dtostrf(convert_kmph2mph(wind_avg_km_h), 3, 1, wind); - line3 += "wind: " + (String)wind + "mp/h "; - } - } - - line3.toCharArray(message->line3, OLED_TEXT_WIDTH); - - // Line 4 - - String rssi = data["rssi"]; - String battery_ok = data["battery_ok"]; - - String line4 = "batt: " + battery_ok + " rssi: " + rssi; - line4.toCharArray(message->line4, OLED_TEXT_WIDTH); - - // Queue completed message - - if (xQueueSend(displayQueue, (void*)&message, 0) != pdTRUE) { - Log.error(F("[ SSD1306 ] displayQueue full, discarding signal %s" CR), message->title); - free(message); - } else { - // Log.notice(F("Queued %s" CR), message->title); - } - break; - } -# endif -# ifdef ZsensorBME280 - case hash("CLIMAtoMQTT"): { - // {"tempc":17.06,"tempf":62.708,"hum":50.0752,"pa":98876.14,"altim":205.8725,"altift":675.4348} - - // Line 1 - - strlcpy(message->line1, "bme280", OLED_TEXT_WIDTH); - - // Line 2 - - String line2 = ""; - if (data.containsKey("tempc")) { - char temp[5]; - float temperature_C = data["tempc"]; - - if (displayMetric) { - dtostrf(temperature_C, 3, 1, temp); - line2 = "temp: " + (String)temp + "°C "; - } else { - dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp); - line2 = "temp: " + (String)temp + "°F "; - } - } - line2.toCharArray(message->line2, OLED_TEXT_WIDTH); - - // Line 3 - - String line3 = ""; - float humidity = data["hum"]; - if (data.containsKey("hum") && humidity <= 100 && humidity >= 0) { - char hum[5]; - dtostrf(humidity, 3, 1, hum); - line3 += "hum: " + (String)hum + "% "; - } - line3.toCharArray(message->line3, OLED_TEXT_WIDTH); - - // Line 4 - - float pa = (int)data["pa"] / 100; - char pressure[6]; - - String line4 = ""; - if (displayMetric) { - dtostrf(pa, 3, 1, pressure); - line4 = "pressure: " + (String)pressure + " hPa"; - } else { - dtostrf(convert_hpa2inhg(pa), 3, 1, pressure); - line4 = "pressure: " + (String)pressure + " inHg"; - } - line4.toCharArray(message->line4, OLED_TEXT_WIDTH); - - // Queue completed message - - if (xQueueSend(displayQueue, (void*)&message, 0) != pdTRUE) { - Log.error(F("[ SSD1306 ] displayQueue full, discarding signal %s" CR), message->title); - free(message); - } else { - // Log.notice(F("Queued %s" CR), message->title); - } - break; - } -# endif -# ifdef ZgatewayBT - case hash("BTtoMQTT"): { - // {"id":"AA:BB:CC:DD:EE:FF","mac_type":0,"adv_type":0,"name":"sps","manufacturerdata":"de071f1000b1612908","rssi":-70,"brand":"Inkbird","model":"T(H) Sensor","model_id":"IBS-TH1/TH2/P01B","type":"THBX","cidc":false,"acts":true,"tempc":20.14,"tempf":68.252,"hum":41.27,"batt":41} - - if (data["model_id"] != "MS-CDP" && data["model_id"] != "GAEN" && data["model_id"] != "APPLE_CONT" && data["model_id"] != "IBEACON") { - // Line 2, 3, 4 - String line2 = ""; - String line3 = ""; - String line4 = ""; - - // Properties - String properties[6] = {"", "", "", "", "", ""}; - int property = -1; - - if (data["type"] == "THB" || data["type"] == "THBX" || data["type"] == "PLANT" || data["type"] == "AIR" || data["type"] == "BATT" || data["type"] == "ACEL" || (data["type"] == "UNIQ" && data["model_id"] == "SDLS")) { - if (data.containsKey("tempc")) { - property++; - char temp[5]; - if (displayMetric) { - float temperature = data["tempc"]; - dtostrf(temperature, 3, 1, temp); - properties[property] = "temp: " + (String)temp + "°C "; - } else { - float temperature = data["tempf"]; - dtostrf(temperature, 3, 1, temp); - properties[property] = "temp: " + (String)temp + "°F "; - } - } - - if (data.containsKey("tempc2_dp")) { - property++; - char tempdp[5]; - if (displayMetric) { - float temperature = data["tempc2_dp"]; - dtostrf(temperature, 3, 1, tempdp); - properties[property] = "dewp: " + (String)tempdp + "°C "; - } else { - float temperature = data["tempf2_dp"]; - dtostrf(temperature, 3, 1, tempdp); - properties[property] = "dewp: " + (String)tempdp + "°F "; - } - } - - if (data.containsKey("extprobe")) { - property++; - properties[property] = " ext. probe"; - } - - if (data.containsKey("hum")) { - property++; - float humidity = data["hum"]; - char hum[5]; - - dtostrf(humidity, 3, 1, hum); - properties[property] = "hum: " + (String)hum + "% "; - } - - if (data.containsKey("pm25")) { - property++; - int pm25int = data["pm25"]; - char pm25[3]; - itoa(pm25int, pm25, 10); - if ((data.containsKey("pm10"))) { - properties[property] = "PM 2.5: " + (String)pm25 + " "; - - } else { - properties[property] = "pm2.5: " + (String)pm25 + "μg/m³ "; - } - } - - if (data.containsKey("pm10")) { - property++; - int pm10int = data["pm10"]; - char pm10[3]; - itoa(pm10int, pm10, 10); - if ((data.containsKey("pm25"))) { - properties[property] = "/ 10: " + (String)pm10 + "μg/m³ "; - - } else { - properties[property] = "pm10: " + (String)pm10 + "μg/m³ "; - } - } - - if (data.containsKey("for")) { - property++; - int formint = data["for"]; - char form[3]; - itoa(formint, form, 10); - properties[property] = "CH₂O: " + (String)form + "mg/m³ "; - } - - if (data.containsKey("co2")) { - property++; - int co2int = data["co2"]; - char co2[4]; - itoa(co2int, co2, 10); - properties[property] = "co2: " + (String)co2 + "ppm "; - } - - if (data.containsKey("moi")) { - property++; - int moiint = data["moi"]; - char moi[4]; - itoa(moiint, moi, 10); - properties[property] = "moi: " + (String)moi + "% "; - } - - if (data.containsKey("lux")) { - property++; - int luxint = data["lux"]; - char lux[5]; - itoa(luxint, lux, 10); - properties[property] = "lux: " + (String)lux + "lx "; - } - - if (data.containsKey("fer")) { - property++; - int ferint = data["fer"]; - char fer[7]; - itoa(ferint, fer, 10); - properties[property] = "fer: " + (String)fer + "µS/cm "; - } - - if (data.containsKey("pres")) { - property++; - int presint = data["pres"]; - char pres[4]; - itoa(presint, pres, 10); - properties[property] = "pres: " + (String)pres + "hPa "; - } - - if (data.containsKey("batt")) { - property++; - int battery = data["batt"]; - char batt[5]; - itoa(battery, batt, 10); - properties[property] = "batt: " + (String)batt + "% "; - } - - if (data.containsKey("shake")) { - property++; - int shakeint = data["shake"]; - char shake[3]; - itoa(shakeint, shake, 10); - properties[property] = "shake: " + (String)shake + " "; - } - - if (data.containsKey("volt")) { - property++; - float voltf = data["volt"]; - char volt[5]; - dtostrf(voltf, 3, 1, volt); - properties[property] = "volt: " + (String)volt + "V "; - } - - if (data.containsKey("wake")) { - property++; - String wakestr = data["wake"]; - properties[property] = "wake: " + wakestr + " "; - } - - } else if (data["type"] == "BBQ") { - String tempcstr = ""; - int j = 7; - if (data["model_id"] == "IBT-2X(S)") { - j = 3; - } else if (data["model_id"] == "IBT-4X(S/C)") { - j = 5; - } - - for (int i = 0; i < j; i++) { - if (i == 0) { - if (displayMetric) { - tempcstr = "tempc"; - } else { - tempcstr = "tempf"; - } - i++; - } else { - if (displayMetric) { - tempcstr = "tempc" + (String)i; - } else { - tempcstr = "tempf" + (String)i; - } - } - - if (data.containsKey(tempcstr)) { - char temp[5]; - float temperature = data[tempcstr]; - dtostrf(temperature, 3, 1, temp); - properties[i - 1] = "tp" + (String)i + ": " + (String)temp; - if (displayMetric) { - properties[i - 1] += "°C "; - } else { - properties[i - 1] += "°F "; - } - } else { - properties[i - 1] = "tp" + (String)i + ": " + "off "; - } - } - } else if (data["type"] == "BODY") { - if (data.containsKey("steps")) { - property++; - int stepsint = data["steps"]; - char steps[5]; - itoa(stepsint, steps, 10); - properties[property] = "steps: " + (String)steps + " "; - // next line - property++; - } - - if (data.containsKey("act_bpm")) { - property++; - int actbpmint = data["act_bpm"]; - char actbpm[3]; - itoa(actbpmint, actbpm, 10); - properties[property] = "activity bpm: " + (String)actbpm + " "; - } - - if (data.containsKey("bpm")) { - property++; - int bpmint = data["bpm"]; - char bpm[3]; - itoa(bpmint, bpm, 10); - properties[property] = "bpm: " + (String)bpm + " "; - } - } else if (data["type"] == "SCALE") { - if (data.containsKey("weighing_mode")) { - property++; - String mode = data["weighing_mode"]; - properties[property] = mode + " "; - // next line - property++; - } - - if (data.containsKey("weight")) { - property++; - float weightf = data["weight"]; - char weight[7]; - dtostrf(weightf, 3, 1, weight); - if (data.containsKey("unit")) { - String unit = data["unit"]; - properties[property] = "weight: " + (String)weight + unit + " "; - } else { - properties[property] = "weight: " + (String)weight; - } - // next line - property++; - } - - if (data.containsKey("impedance")) { - property++; - int impint = data["impedance"]; - char imp[3]; - itoa(impint, imp, 10); - properties[property] = "impedance: " + (String)imp + "ohm "; - } - } else if (data["type"] == "UNIQ") { - if (data["model_id"] == "M1017") { - if (data.containsKey("lvl_cm")) { - property++; - char lvl[5]; - if (displayMetric) { - float lvlf = data["lvl_cm"]; - dtostrf(lvlf, 3, 1, lvl); - properties[property] = "level: " + (String)lvl + "cm "; - } else { - float lvlf = data["lvl_in"]; - dtostrf(lvlf, 3, 1, lvl); - properties[property] = "level: " + (String)lvl + "\" "; - } - } - - if (data.containsKey("quality")) { - property++; - int qualint = data["quality"]; - char qual[3]; - itoa(qualint, qual, 10); - properties[property] = "qy: " + (String)qual + " "; - } - - if (data.containsKey("batt")) { - property++; - int battery = data["batt"]; - char batt[5]; - itoa(battery, batt, 10); - properties[property] = "batt: " + (String)batt + "% "; - } - } - } - - line2 = properties[0] + properties[1]; - line3 = properties[2] + properties[3]; - line4 = properties[4] + properties[5]; - - if (!(line2 == "" && line3 == "" && line4 == "")) { - // Titel - char* topic = strdup(topicori); - String heading = strtok(topic, "/"); - String line0 = heading + " " + data["id"].as().substring(9, 17); - line0.toCharArray(message->title, OLED_TEXT_WIDTH); - free(topic); - - // Line 1 - strlcpy(message->line1, data["model"], OLED_TEXT_WIDTH); - - line2.toCharArray(message->line2, OLED_TEXT_WIDTH); - line3.toCharArray(message->line3, OLED_TEXT_WIDTH); - line4.toCharArray(message->line4, OLED_TEXT_WIDTH); - - if (xQueueSend(displayQueue, (void*)&message, 0) != pdTRUE) { - Log.error(F("[ SSD1306 ] displayQueue full, discarding signal %s" CR), message->title); - free(message); - } else { - // Log.notice(F("Queued %s" CR), message->title); - } - } else { - free(message); - } - - break; - } else { - free(message); - } - } -# endif - default: - Log.error(F("[ SSD1306 ] unhandled topic %s" CR), message->title); - } - } else { - Log.error(F("[ SSD1306 ] insufficent memory " CR)); - } - } -} - // Simple print methonds /* @@ -825,7 +283,6 @@ void ssd1306Print(char* line1) { OledSerial Oled(0); // Not sure about this, came from Hardwareserial OledSerial::OledSerial(int x) { - displayQueue = xQueueCreate(5, sizeof(displayQueueMessage*)); # if defined(WIFI_Kit_32) || defined(WIFI_LoRa_32) || defined(WIFI_LoRa_32_V2) pinMode(RST_OLED, OUTPUT); // https://github.com/espressif/arduino-esp32/issues/4278 digitalWrite(RST_OLED, LOW); @@ -939,7 +396,7 @@ size_t OledSerial::write(const uint8_t* buffer, size_t size) { Display full page message on the display. - Used to display JSON messages published from each gateway module */ -boolean OledSerial::displayPage(displayQueueMessage* message) { +boolean OledSerial::displayPage(webUIQueueMessage* message) { if (xPortGetCoreID() == CONFIG_ARDUINO_RUNNING_CORE) { if (xSemaphoreTake(semaphoreOLEDOperation, pdMS_TO_TICKS(30000)) == pdTRUE) { display->normalDisplay(); @@ -1004,7 +461,6 @@ String stateSSD1306Display() { JsonObject DISPLAYdata = jsonBuffer.to(); DISPLAYdata["onstate"] = (bool)displayState; DISPLAYdata["brightness"] = (int)displayBrightness; - DISPLAYdata["displaymetric"] = (bool)displayMetric; DISPLAYdata["display-flip"] = (bool)displayFlip; DISPLAYdata["idlelogo"] = (bool)idlelogo; DISPLAYdata["log-oled"] = (bool)logToOLEDDisplay; diff --git a/main/ZgatewayBT.ino b/main/ZgatewayBT.ino index e722e32..590e992 100644 --- a/main/ZgatewayBT.ino +++ b/main/ZgatewayBT.ino @@ -104,7 +104,7 @@ void BTConfig_init() { unsigned long timeBetweenConnect = 0; unsigned long timeBetweenActive = 0; -void stateBTMeasures(bool start) { +String stateBTMeasures(bool start) { StaticJsonDocument jsonBuffer; JsonObject jo = jsonBuffer.to(); jo["bleconnect"] = BTConfig.bleConnect; @@ -135,9 +135,12 @@ void stateBTMeasures(bool start) { Log.notice(F("BT sys: ")); serializeJsonPretty(jsonBuffer, Serial); Serial.println(); - return; // Do not try to erase/write/send config at startup + return ""; // Do not try to erase/write/send config at startup } + String output; + serializeJson(jo, output); pub(subjectBTtoMQTT, jo); + return (output); } void BTConfig_fromJson(JsonObject& BTdata, bool startup = false) { @@ -1091,9 +1094,6 @@ void PublishDeviceData(JsonObject& BLEdata, bool processBLEData) { if (!BTConfig.pubRandomMACs && (BLEdata["type"].as()).compare("RMAC") == 0) { return; } - if (BLEdata.containsKey("type") && (BLEdata.containsKey("model") || BLEdata.containsKey("distance"))) { // Only display sensor data with type - pubOled(subjectBTtoMQTT, BLEdata); - } if (!BTConfig.pubAdvData) { BLEdata.remove("servicedatauuid"); BLEdata.remove("servicedata"); @@ -1101,7 +1101,7 @@ void PublishDeviceData(JsonObject& BLEdata, bool processBLEData) { BLEdata.remove("mac_type"); BLEdata.remove("adv_type"); // tag device properties - BLEdata.remove("type"); + // BLEdata.remove("type"); type is used by the WebUI module to determine the template used to display the signal BLEdata.remove("cidc"); BLEdata.remove("acts"); BLEdata.remove("cont"); diff --git a/main/ZsensorBME280.ino b/main/ZsensorBME280.ino index b7d05d3..b82e21b 100644 --- a/main/ZsensorBME280.ino +++ b/main/ZsensorBME280.ino @@ -193,7 +193,6 @@ void MeasureTempHumAndPressure() { } if (BME280data.size() > 0) { pub(BMETOPIC, BME280data); - pubOled(BMETOPIC, BME280data); } } diff --git a/main/ZwebUI.ino b/main/ZwebUI.ino new file mode 100644 index 0000000..593301c --- /dev/null +++ b/main/ZwebUI.ino @@ -0,0 +1,1632 @@ +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a wifi or ethernet gateway between your 433mhz/infrared IR signal and a MQTT broker + Send and receiving command by MQTT + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include "User_config.h" +#if defined(ZwebUI) && defined(ESP32) +# include +# include +# include // Docs for this are here - https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer + +# include "ArduinoLog.h" +# include "config_WebContent.h" +# include "config_WebUI.h" + +# if defined(ZgatewayCloud) +# include "config_Cloud.h" +# endif + +# if defined(ZdisplaySSD1306) +# include "config_SSD1306.h" +# endif + +uint32_t requestToken = 0; + +QueueHandle_t webUIQueue; + +WebServer server(80); + +boolean displayMetric = DISPLAY_METRIC; + +/*------------------- Local functions ----------------------*/ + +void notFound(); +void handleRoot(); // "/" + +void handleCS(); // Console +void handleCN(); // Configuration + +void handleIN(); // Information + +void handleRT(); // Restart +void handleCL(); // Configure Cloud +void handleTK(); // Return Cloud token + +/*------------------- External functions ----------------------*/ + +esp_err_t nvs_flash_erase(void); +extern void eraseAndRestart(); +extern unsigned long uptime(); + +/*------------------- Web Console Globals ----------------------*/ + +# define ROW_LENGTH 1024 + +const uint16_t LOG_BUFFER_SIZE = 6096; +uint32_t log_buffer_pointer; +void* log_buffer_mutex; +char log_buffer[LOG_BUFFER_SIZE]; // Log buffer in DRAM + +const uint16_t MAX_LOGSZ = LOG_BUFFER_SIZE - 96; +const uint16_t TOPSZ = 151; // Max number of characters in topic string +uint8_t masterlog_level; // Master log level used to override set log level +bool reset_web_log_flag = false; // Reset web console log + +# ifdef ESP32 +/*********************************************************************************************\ + * ESP32 AutoMutex +\*********************************************************************************************/ + +////////////////////////////////////////// +// automutex. +// create a mute in your driver with: +// void *mutex = nullptr; +// +// then protect any function with +// TasAutoMutex m(&mutex, "somename"); +// - mutex is automatically initialised if not already intialised. +// - it will be automagically released when the function is over. +// - the same thread can take multiple times (recursive). +// - advanced options m.give() and m.take() allow you fine control within a function. +// - if take=false at creat, it will not be initially taken. +// - name is used in serial log of mutex deadlock. +// - maxWait in ticks is how long it will wait before failing in a deadlock scenario (and then emitting on serial) +class TasAutoMutex { + SemaphoreHandle_t mutex; + bool taken; + int maxWait; + const char* name; + +public: + TasAutoMutex(SemaphoreHandle_t* mutex, const char* name = "", int maxWait = 40, bool take = true); + ~TasAutoMutex(); + void give(); + void take(); + static void init(SemaphoreHandle_t* ptr); +}; +////////////////////////////////////////// + +TasAutoMutex::TasAutoMutex(SemaphoreHandle_t* mutex, const char* name, int maxWait, bool take) { + if (mutex) { + if (!(*mutex)) { + TasAutoMutex::init(mutex); + } + this->mutex = *mutex; + this->maxWait = maxWait; + this->name = name; + if (take) { + this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait); + // if (!this->taken){ + // Serial.printf("\r\nMutexfail %s\r\n", this->name); + // } + } + } else { + this->mutex = (SemaphoreHandle_t) nullptr; + } +} + +TasAutoMutex::~TasAutoMutex() { + if (this->mutex) { + if (this->taken) { + xSemaphoreGiveRecursive(this->mutex); + this->taken = false; + } + } +} + +void TasAutoMutex::init(SemaphoreHandle_t* ptr) { + SemaphoreHandle_t mutex = xSemaphoreCreateRecursiveMutex(); + (*ptr) = mutex; + // needed, else for ESP8266 as we will initialis more than once in logging + // (*ptr) = (void *) 1; +} + +void TasAutoMutex::give() { + if (this->mutex) { + if (this->taken) { + xSemaphoreGiveRecursive(this->mutex); + this->taken = false; + } + } +} + +void TasAutoMutex::take() { + if (this->mutex) { + if (!this->taken) { + this->taken = xSemaphoreTakeRecursive(this->mutex, this->maxWait); + // if (!this->taken){ + // Serial.printf("\r\nMutexfail %s\r\n", this->name); + // } + } + } +} + +// Get span until single character in string +size_t strchrspn(const char* str1, int character) { + size_t ret = 0; + char* start = (char*)str1; + char* end = strchr(str1, character); + if (end) ret = end - start; + return ret; +} + +# endif // ESP32 + +void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_payload = nullptr, const char* log_data_retained = nullptr) { + // Store log_data in buffer + // To lower heap usage log_data_payload may contain the payload data from MqttPublishPayload() + // and log_data_retained may contain optional retained message from MqttPublishPayload() +# ifdef ESP32 + // this takes the mutex, and will be release when the class is destroyed - + // i.e. when the functon leaves You CAN call mutex.give() to leave early. + TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex); +# endif // ESP32 + + char empty[2] = {0}; + if (!log_data_payload) { + log_data_payload = empty; + } + if (!log_data_retained) { + log_data_retained = empty; + } + + if (!log_buffer) { + return; + } // Leave now if there is no buffer available + + // Delimited, zero-terminated buffer of log lines. + // Each entry has this format: [index][loglevel][log data]['\1'] + + // Truncate log messages longer than MAX_LOGSZ which is the log buffer size minus 64 spare + uint32_t log_data_len = strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained); + char too_long[TOPSZ]; + if (log_data_len > MAX_LOGSZ) { + snprintf_P(too_long, sizeof(too_long) - 20, PSTR("%s%s"), log_data, log_data_payload); // 20 = strlen("... 123456 truncated") + snprintf_P(too_long, sizeof(too_long), PSTR("%s... %d truncated"), too_long, log_data_len); + log_data = too_long; + log_data_payload = empty; + log_data_retained = empty; + } + + log_buffer_pointer &= 0xFF; + if (!log_buffer_pointer) { + log_buffer_pointer++; // Index 0 is not allowed as it is the end of char string + } + while (log_buffer_pointer == log_buffer[0] || // If log already holds the next index, remove it + strlen(log_buffer) + strlen(log_data) + strlen(log_data_payload) + strlen(log_data_retained) + 4 > LOG_BUFFER_SIZE) // 4 = log_buffer_pointer + '\1' + '\0' + { + char* it = log_buffer; + it++; // Skip log_buffer_pointer + it += strchrspn(it, '\1'); // Skip log line + it++; // Skip delimiting "\1" + memmove(log_buffer, it, LOG_BUFFER_SIZE - (it - log_buffer)); // Move buffer forward to remove oldest log line + } + snprintf_P(log_buffer, LOG_BUFFER_SIZE, PSTR("%s%c%c%s%s%s%s\1"), + log_buffer, log_buffer_pointer++, '0' + loglevel, "", log_data, log_data_payload, log_data_retained); + log_buffer_pointer &= 0xFF; + if (!log_buffer_pointer) { + log_buffer_pointer++; // Index 0 is not allowed as it is the end of char string + } +} + +bool NeedLogRefresh(uint32_t req_loglevel, uint32_t index) { + if (!log_buffer) { + return false; + } // Leave now if there is no buffer available + +# ifdef ESP32 + // this takes the mutex, and will be release when the class is destroyed - + // i.e. when the functon leaves You CAN call mutex.give() to leave early. + TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex); +# endif // ESP32 + + // Skip initial buffer fill + if (strlen(log_buffer) < LOG_BUFFER_SIZE / 2) { + return false; + } + + char* line; + size_t len; + if (!GetLog(req_loglevel, &index, &line, &len)) { + return false; + } + return ((line - log_buffer) < LOG_BUFFER_SIZE / 4); +} + +bool GetLog(uint32_t req_loglevel, uint32_t* index_p, char** entry_pp, size_t* len_p) { + if (!log_buffer) { + return false; + } // Leave now if there is no buffer available + if (uptime() < 3) { + return false; + } // Allow time to setup correct log level + + uint32_t index = *index_p; + if (!req_loglevel || (index == log_buffer_pointer)) { + return false; + } + +# ifdef ESP32 + // this takes the mutex, and will be release when the class is destroyed - + // i.e. when the functon leaves You CAN call mutex.give() to leave early. + TasAutoMutex mutex((SemaphoreHandle_t*)&log_buffer_mutex); +# endif // ESP32 + + if (!index) { // Dump all + index = log_buffer[0]; + } + + do { + size_t len = 0; + uint32_t loglevel = 0; + char* entry_p = log_buffer; + do { + uint32_t cur_idx = *entry_p; + entry_p++; + size_t tmp = strchrspn(entry_p, '\1'); + tmp++; // Skip terminating '\1' + if (cur_idx == index) { // Found the requested entry + loglevel = *entry_p - '0'; + entry_p++; // Skip loglevel + len = tmp - 1; + break; + } + entry_p += tmp; + } while (entry_p < log_buffer + LOG_BUFFER_SIZE && *entry_p != '\0'); + index++; + if (index > 255) { + index = 1; + } // Skip 0 as it is not allowed + *index_p = index; + if ((len > 0) && + (loglevel <= req_loglevel) && + (masterlog_level <= req_loglevel)) { + *entry_pp = entry_p; + *len_p = len; + return true; + } + delay(0); + } while (index != log_buffer_pointer); + return false; +} + +/*------------------- Local functions ----------------------*/ + +# ifdef WEBUI_DEVELOPMENT +//format bytes +String formatBytes(size_t bytes) { + if (bytes < 1024) { + return String(bytes) + "B"; + } else if (bytes < (1024 * 1024)) { + return String(bytes / 1024.0) + "KB"; + } else if (bytes < (1024 * 1024 * 1024)) { + return String(bytes / 1024.0 / 1024.0) + "MB"; + } else { + return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB"; + } +} + +bool exists(String path) { + bool yes = false; + File file = FILESYSTEM.open(path, "r"); + if (!file.isDirectory()) { + yes = true; + } + file.close(); + return yes; +} +# endif + +/** + * @brief / - Page + * + */ +void handleRoot() { + WEBUI_TRACE_LOG(F("handleRoot: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + if (server.hasArg("m")) { + if (currentWebUIMessage) { + server.send(200, "application/json", "{t}{s}" + String(currentWebUIMessage->title) + "{e}{s}" + String(currentWebUIMessage->line1) + "{e}{s}" + String(currentWebUIMessage->line2) + "{e}{s}" + String(currentWebUIMessage->line3) + "{e}{s}" + String(currentWebUIMessage->line4) + "{e}"); + } else { + server.send(200, "application/json", "{t}{s}Uptime:{m}" + String(uptime()) + "{e}"); + } + } else if (server.hasArg("rst")) { // TODO: This should redirect to the RST page + Log.warning(F("[WebUI] Restart" CR)); + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Restart").c_str()); + String response = String(buffer); + response += String(restart_script); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, reset_body, jsonChar, gateway_name, "Restart"); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + + delay(2000); // Wait for web page to be sent before + +# if defined(ESP8266) + ESP.reset(); +# else + ESP.restart(); +# endif + } else { + // WEBUI_TRACE_LOG(F("Arguments %s" CR), message); + server.send(200, "text/plain", "00:14:36.767 RSL: RESULT = {\"Topic\":\"topic\"}"); + } + } else { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Main Menu").c_str()); + String response = String(buffer); + response += String(root_script); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, root_body, jsonChar, gateway_name); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + } +} + +/** + * @brief /CN - Configuration Page + * + */ +void handleCN() { + WEBUI_TRACE_LOG(F("handleCN: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleCN Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } else { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Configuration").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_body, jsonChar, gateway_name); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + } +} + +/** + * @brief /WI - Configure WiFi Page + * + */ +void handleWI() { + WEBUI_TRACE_LOG(F("handleWI: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleWI Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Configure WiFi").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_wifi_body, jsonChar, gateway_name, "", "", "", "", "", ""); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); +} + +/** + * @brief /MQ - Configure MQTT Page + * + */ +void handleMQ() { + WEBUI_TRACE_LOG(F("handleMQ: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleMQ Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Configure MQTT").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + // mqtt server, mqtt port, client id, mqtt username, mqtt password, topic, Full Topic + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_mqtt_body, jsonChar, gateway_name, mqtt_server, mqtt_port, gateway_name, mqtt_user, mqtt_pass, mqtt_topic, ""); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); +} + +/** + * @brief /RT - Reset configuration ( Erase and Restart ) from Configuration menu + * + */ +void handleRT() { + WEBUI_TRACE_LOG(F("handleRT: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleRT Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } + if (server.hasArg("non")) { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + Log.warning(F("[WebUI] Erase and Restart" CR)); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Erase and Restart").c_str()); + String response = String(buffer); + response += String(restart_script); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, reset_body, jsonChar, gateway_name, "Erase and Restart"); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + + nvs_flash_erase(); + ESP.restart(); + } else { + handleCN(); + } +} + +# if defined(ZgatewayCloud) +/** + * @brief /CL - Cloud Configuration + * + */ +void handleCL() { + WEBUI_TRACE_LOG(F("handleCL: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleCL Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } + + if (server.hasArg("save")) { + // T: handleCL: uri: /cl, args: 2, method: 1 + // T: handleCL Arg: 0, cl-en=on + // T: handleCL Arg: 1, save= + if (server.hasArg("save") && server.method() == 1) { + setCloudEnabled(server.hasArg("cl-en")); + } + } + + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Configure Cloud").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + + char cloudEnabled[8] = {0}; + if (isCloudEnabled()) { + strncpy(cloudEnabled, "checked", 8); + } + + char deviceToken[5] = {0}; + if (!isCloudDeviceTokenSupplied()) { + strncpy(deviceToken, " Not", 4); + } + + requestToken = esp_random(); +# ifdef ESP32_ETHERNET + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_cloud_body, jsonChar, gateway_name, " cloud checked", " Not", (String(CLOUDGATEWAY) + "token/start").c_str(), (char*)ETH.macAddress().c_str(), ("http://" + String(ip2CharArray(ETH.localIP())) + "/").c_str(), gateway_name, uptime(), requestToken); +# else + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, config_cloud_body, jsonChar, gateway_name, cloudEnabled, deviceToken, (String(CLOUDGATEWAY) + "token/start").c_str(), (char*)WiFi.macAddress().c_str(), ("http://" + String(ip2CharArray(WiFi.localIP())) + "/").c_str(), gateway_name, uptime(), requestToken); +# endif + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); +} + +/** + * @brief /TK - Receive Cloud Device Token + * + */ +void handleTK() { + WEBUI_TRACE_LOG(F("handleTK: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleTK Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } + + if (server.hasArg("deviceToken") && server.hasArg("uptime") && server.hasArg("RT")) { + String deviceToken = server.arg("deviceToken"); + + if (setCloudDeviceToken(deviceToken) && server.arg("RT").toInt() == requestToken && server.arg("uptime").toInt() + 600 > uptime()) { + setCloudEnabled(true); + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Received Device Token").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, token_body, jsonChar, gateway_name); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + } else { + WEBUI_TRACE_LOG(F("handleTK: uptime: %u, uptime: %u, ok: %T" CR), server.arg("uptime").toInt(), uptime(), server.arg("uptime").toInt() + 600 > uptime()); + WEBUI_TRACE_LOG(F("handleTK: RT: %d, RT: %d, ok: %T " CR), server.arg("RT").toInt(), requestToken, server.arg("RT").toInt() == requestToken); + Log.error(F("[WebUI] Invalid Token Response: RT: %T, uptime: %T" CR), server.arg("RT").toInt() == requestToken, server.arg("uptime").toInt() + 600 > uptime()); + server.send(500, "text/html", "Internal ERROR - Invalid Token"); + } + } +} + +# endif + +/** + * @brief /IN - Information Page + * + */ +void handleIN() { + WEBUI_TRACE_LOG(F("handleCN: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleIN Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } else { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + String informationDisplay = stateMeasures(); // .replace(",\"", "}1"); // .replace("\":", "=2") + +// }1 json-oled }2 true } }1 Cloud }2 cloudEnabled}2true}1c +# if defined(ZgatewayBT) + informationDisplay += "1
BT}2}1"; // }1 the bracket is not needed as the previous message ends with } + informationDisplay += stateBTMeasures(false); +# endif +# if defined(ZdisplaySSD1306) + informationDisplay += "1
SSD1306}2}1"; // }1 the bracket is not needed as the previous message ends with } + informationDisplay += stateSSD1306Display(); +# endif +# if defined(ZgatewayCloud) + informationDisplay += "1
Cloud}2}1"; + informationDisplay += stateCLOUDStatus(); +# endif + informationDisplay += "1
WebUI}2}1"; + informationDisplay += stateWebUIStatus(); + + // stateBTMeasures causes a Stack canary watchpoint triggered (loopTask) + // WEBUI_TRACE_LOG(F("[WebUI] informationDisplay before %s" CR), informationDisplay.c_str()); + + // TODO: need to fix display of modules array within SYStoMQTT + + informationDisplay += "1}2"; + informationDisplay.replace(",\"", "}1"); + informationDisplay.replace("\":", "}2"); + informationDisplay.replace("{\"", ""); + informationDisplay.replace("\"", "\\\""); + + // WEBUI_TRACE_LOG(F("[WebUI] informationDisplay after %s" CR), informationDisplay.c_str()); + + if (informationDisplay.length() > WEB_TEMPLATE_BUFFER_MAX_SIZE) { + Log.warning(F("[WebUI] informationDisplay content length ( %d ) greater than WEB_TEMPLATE_BUFFER_MAX_SIZE. Display truncated" CR), informationDisplay.length()); + } + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Information").c_str()); + String response = String(buffer); + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, information_script, informationDisplay.c_str()); + response += String(buffer); + + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, information_body, jsonChar, gateway_name); + response += String(buffer); + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + + server.send(200, "text/html", response); + } +} + +/** + * @brief /UP - Firmware Upgrade Page + * + */ +void handleUP() { + WEBUI_TRACE_LOG(F("handleUP: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args()) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleUP Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + } else { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Firmware Upgrade").c_str()); + String response = String(buffer); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, upgrade_body, jsonChar, gateway_name, "http://upgrade/"); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + } +} + +uint32_t logIndex = 0; + +/** + * @brief /CS - Serial Console and Command Line + * + */ +void handleCS() { + WEBUI_TRACE_LOG(F("handleCS: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + if (server.args() && server.hasArg("c2")) { + for (uint8_t i = 0; i < server.args(); i++) { + WEBUI_TRACE_LOG(F("handleCS Arg: %d, %s=%s" CR), i, server.argName(i).c_str(), server.arg(i).c_str()); + } + if (server.hasArg("c1")) { + String c1 = server.arg("c1"); + + String cmdTopic = String(mqtt_topic) + String(gateway_name) + "/" + c1.substring(0, c1.indexOf(' ')); + String command = c1.substring(c1.indexOf(' ') + 1); + if (command.length()) { + WEBUI_TRACE_LOG(F("[WebUI] handleCS inject MQTT Command topic: '%s', command: '%s'" CR), cmdTopic.c_str(), command.c_str()); + receivingMQTT((char*)cmdTopic.c_str(), (char*)command.c_str()); + } else { + Log.warning(F("[WebUI] Missing command: '%s', command: '%s'" CR), cmdTopic.c_str(), command.c_str()); + } + } + + uint32_t index = server.arg("c2").toInt(); + + String message = String(log_buffer_pointer) + "}1" + String(reset_web_log_flag) + "}1"; + if (!reset_web_log_flag) { + index = 0; + reset_web_log_flag = true; + } + + bool cflg = (index); + char* line; + size_t len; + while (GetLog(1, &index, &line, &len)) { + if (cflg) { + message += "\n"; + } + message += String(line, len - 1); + cflg = true; + } + message += "}1"; + server.send(200, "text/plain", message); + } else { + char jsonChar[100]; + serializeJson(modules, jsonChar, measureJson(modules) + 1); + + char buffer[WEB_TEMPLATE_BUFFER_MAX_SIZE]; + + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, header_html, (String(gateway_name) + " - Configuration").c_str()); + String response = String(buffer); + response += String(console_script); + response += String(script); + response += String(style); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, console_body, jsonChar, gateway_name); + response += String(buffer); + snprintf(buffer, WEB_TEMPLATE_BUFFER_MAX_SIZE, footer, OMG_VERSION); + response += String(buffer); + server.send(200, "text/html", response); + } +} + +/** + * @brief Page not found handler + * + */ +void notFound() { +# ifdef WEBUI_DEVELOPMENT + String path = server.uri(); + if (!exists(path)) { + if (exists(path + ".html")) { + path += ".html"; + } else { +# endif + Log.warning(F("[WebUI] notFound: uri: %s, args: %d, method: %d" CR), server.uri(), server.args(), server.method()); + server.send(404, "text/plain", "Not found"); + return; +# ifdef WEBUI_DEVELOPMENT + } + } + WEBUI_TRACE_LOG(F("notFound returning: actual uri: %s, args: %d, method: %d" CR), path, server.args(), server.method()); + File file = FILESYSTEM.open(path, "r"); + server.streamFile(file, "text/html"); + file.close(); +# endif +} + +void WebUISetup() { + WEBUI_TRACE_LOG(F("ZwebUI setup start" CR)); + + webUIQueue = xQueueCreate(5, sizeof(webUIQueueMessage*)); + +# ifdef WEBUI_DEVELOPMENT + FILESYSTEM.begin(); + { + File root = FILESYSTEM.open("/"); + File file = root.openNextFile(); + while (file) { + String fileName = file.name(); + size_t fileSize = file.size(); + WEBUI_TRACE_LOG(F("FS File: %s, size: %s" CR), fileName.c_str(), formatBytes(fileSize).c_str()); + file = root.openNextFile(); + } + } +# endif + server.onNotFound(notFound); + + server.on("/", handleRoot); // Main Menu + + server.on("/in", handleIN); // Information + server.on("/cs", handleCS); // Console + server.on("/up", handleUP); // Firmware Upgrade + + server.on("/cn", handleCN); // Configuration + server.on("/wi", handleWI); // Configuration Wifi + server.on("/mq", handleMQ); // Configuration MQTT +# if defined(ZgatewayCloud) + server.on("/cl", handleCL); // Cloud configuration + server.on("/tk", handleTK); // Store Device Token +# endif + + server.on("/rt", handleRT); // Reset configuration ( Erase and Restart ) + server.begin(); + + Log.begin(LOG_LEVEL, &WebLog); + + Log.notice(F("OpenMQTTGateway URL: http://%s/" CR), WiFi.localIP().toString().c_str()); + displayPrint("URL: http://", (char*)WiFi.localIP().toString().c_str()); + Log.notice(F("ZwebUI setup done" CR)); +} + +unsigned long nextWebUIPage = uptime() + DISPLAY_WEBUI_INTERVAL; + +void WebUILoop() { + server.handleClient(); + + if (uptime() >= nextWebUIPage && uxQueueMessagesWaiting(webUIQueue)) { + webUIQueueMessage* message = nullptr; + xQueueReceive(webUIQueue, &message, portMAX_DELAY); + + if (currentWebUIMessage) { + free(currentWebUIMessage); + } + currentWebUIMessage = message; + nextWebUIPage = uptime() + DISPLAY_WEBUI_INTERVAL; + } +} + +void MQTTtoWebUI(char* topicOri, JsonObject& WebUIdata) { // json object decoding + bool success = false; + if (cmpToMainTopic(topicOri, subjectMQTTtoWebUIset)) { + WEBUI_TRACE_LOG(F("MQTTtoWebUI json set" CR)); + // properties + if (WebUIdata.containsKey("displaymetric")) { + displayMetric = WebUIdata["displaymetric"].as(); + Log.notice(F("Set displaymetric: %T" CR), displayMetric); + success = true; + } + // save, load, init, erase + if (WebUIdata.containsKey("save") && WebUIdata["save"]) { + success = WebUIConfig_save(); + if (success) { + Log.notice(F("WebUI config saved" CR)); + } + } else if (WebUIdata.containsKey("load") && WebUIdata["load"]) { + success = WebUIConfig_load(); + if (success) { + Log.notice(F("WebUI config loaded" CR)); + } + } else if (WebUIdata.containsKey("init") && WebUIdata["init"]) { + WebUIConfig_init(); + success = true; + if (success) { + Log.notice(F("WebUI config initialised" CR)); + } + } else if (WebUIdata.containsKey("erase") && WebUIdata["erase"]) { + // Erase config from NVS (non-volatile storage) + preferences.begin(Gateway_Short_Name, false); + success = preferences.remove("WebUIConfig"); + preferences.end(); + if (success) { + Log.notice(F("WebUI config erased" CR)); + } + } + if (success) { + stateWebUIStatus(); + } else { + // pub(subjectWebUItoMQTT, "{\"Status\": \"Error\"}"); // Fail feedback + Log.error(F("[ WebUI ] MQTTtoWebUI Fail json" CR), WebUIdata); + } + } +} + +String stateWebUIStatus() { + //Publish display state + StaticJsonDocument jsonBuffer; + JsonObject WebUIdata = jsonBuffer.to(); + WebUIdata["displayMetric"] = (bool)displayMetric; + WebUIdata["displayQueue"] = uxQueueMessagesWaiting(webUIQueue); + ; + + String output; + serializeJson(WebUIdata, output); + + // WebUIdata["currentMessage"] = currentWebUIMessage; + pub(subjectWebUItoMQTT, WebUIdata); + return output; +} + +bool WebUIConfig_save() { + StaticJsonDocument jsonBuffer; + JsonObject jo = jsonBuffer.to(); + jo["displayMetric"] = (bool)displayMetric; + // Save config into NVS (non-volatile storage) + String conf = ""; + serializeJson(jsonBuffer, conf); + preferences.begin(Gateway_Short_Name, false); + preferences.putString("WebUIConfig", conf); + preferences.end(); + return true; +} + +void WebUIConfig_init() { + boolean displayMetric = DISPLAY_METRIC; + Log.notice(F("WebUI config initialised" CR)); +} + +bool WebUIConfig_load() { + StaticJsonDocument jsonBuffer; + preferences.begin(Gateway_Short_Name, true); + String exists = preferences.getString("WebUIConfig", "{}"); + if (exists != "{}") { + auto error = deserializeJson(jsonBuffer, preferences.getString("WebUIConfig", "{}")); + preferences.end(); + if (error) { + Log.error(F("WebUI config deserialization failed: %s, buffer capacity: %u" CR), error.c_str(), jsonBuffer.capacity()); + return false; + } + if (jsonBuffer.isNull()) { + Log.warning(F("WebUI config is null" CR)); + return false; + } + JsonObject jo = jsonBuffer.as(); + displayMetric = jo["displayMetric"].as(); + return true; + } else { + preferences.end(); + Log.notice(F("No WebUI config to load" CR)); + return false; + } +} + +/* +Workaround for c not having a string based switch/case function +*/ +constexpr unsigned int webUIHash(const char* s, int off = 0) { // workaround for switching on a string https://stackoverflow.com/a/46711735/18643696 + return !s[off] ? 5381 : (webUIHash(s, off + 1) * 33) ^ s[off]; +} + +/* +Parse json message from module into a format for display +*/ +void webUIPubPrint(const char* topicori, JsonObject& data) { + WEBUI_TRACE_LOG(F("[ webUIPubPrint ] pub %s " CR), topicori); + if (webUIQueue) { + webUIQueueMessage* message = (webUIQueueMessage*)heap_caps_calloc(1, sizeof(webUIQueueMessage), MALLOC_CAP_8BIT); + if (message != NULL) { + // Initalize message + strlcpy(message->line1, "", WEBUI_TEXT_WIDTH); + strlcpy(message->line2, "", WEBUI_TEXT_WIDTH); + strlcpy(message->line3, "", WEBUI_TEXT_WIDTH); + strlcpy(message->line4, "", WEBUI_TEXT_WIDTH); + char* topic = strdup(topicori); + strlcpy(message->title, strtok(topic, "/"), WEBUI_TEXT_WIDTH); + free(topic); + + WEBUI_TRACE_LOG(F("[ webUIPubPrint ] switch %s " CR), message->title); + switch (webUIHash(message->title)) { + case webUIHash("SYStoMQTT"): { + // {"uptime":456356,"version":"lilygo-rtl_433-test-A-v1.1.1-25-g574177d[lily-cloud]","freemem":125488,"mqttport":"1883","mqttsecure":false,"freestack":3752,"rssi":-36,"SSID":"The_Beach","BSSID":"64:A5:C3:69:C3:38","ip":"192.168.1.239","mac":"4C:75:25:A8:D5:D8","actRec":3,"mhz":433.92,"RTLRssiThresh":-98,"RTLRssi":-108,"RTLAVGRssi":-107,"RTLCnt":121707,"RTLOOKThresh":90,"modules":["LILYGO_OLED","CLOUD","rtl_433"]} + + // Line 1 + + if (data["version"]) { + strlcpy(message->line1, data["version"], WEBUI_TEXT_WIDTH); + } else { + strlcpy(message->line1, "", WEBUI_TEXT_WIDTH); + } + + // Line 2 + + String uptime = data["uptime"]; + String line = "uptime: " + uptime; + line.toCharArray(message->line2, WEBUI_TEXT_WIDTH); + + // Line 3 + + String freemem = data["freemem"]; + line = "freemem: " + freemem; + line.toCharArray(message->line3, WEBUI_TEXT_WIDTH); + + // Line 4 + + String ip = data["ip"]; + line = "ip: " + ip; + line.toCharArray(message->line4, WEBUI_TEXT_WIDTH); + + // Queue completed message + + if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) { + Log.warning(F("[ WebUI ] ERROR: webUIQueue full, discarding %s" CR), message->title); + free(message); + } else { + // Log.notice(F("[ WebUI ] Queued %s" CR), message->title); + } + break; + } + +# ifdef ZgatewayRTL_433 + case webUIHash("RTL_433toMQTT"): { + // {"model":"Acurite-Tower","id":2043,"channel":"B","battery_ok":1,"temperature_C":5.3,"humidity":81,"mic":"CHECKSUM","protocol":"Acurite 592TXR Temp/Humidity, 5n1 Weather Station, 6045 Lightning, 3N1, Atlas","rssi":-81,"duration":121060} + + // Line 1 + + strlcpy(message->line1, data["model"], WEBUI_TEXT_WIDTH); + + // Line 2 + + String id = data["id"]; + String channel = data["channel"]; + String line2 = "id: " + id + " channel: " + channel; + line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH); + + // Line 3 + + String line3 = ""; + + if (data.containsKey("temperature_C")) { + float temperature_C = data["temperature_C"]; + char temp[5]; + + if (displayMetric) { + dtostrf(temperature_C, 3, 1, temp); + line3 = "temp: " + (String)temp + "°C "; + } else { + dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp); + line3 = "temp: " + (String)temp + "°F "; + } + } + + float humidity = data["humidity"]; + if (data.containsKey("humidity") && humidity <= 100 && humidity >= 0) { + char hum[5]; + dtostrf(humidity, 3, 1, hum); + line3 += "hum: " + (String)hum + "% "; + } + if (data.containsKey("wind_avg_km_h")) { + float wind_avg_km_h = data["wind_avg_km_h"]; + char wind[6]; + + if (displayMetric) { + dtostrf(wind_avg_km_h, 3, 1, wind); + line3 += "wind: " + (String)wind + "km/h "; + } else { + dtostrf(convert_kmph2mph(wind_avg_km_h), 3, 1, wind); + line3 += "wind: " + (String)wind + "mp/h "; + } + } + + line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH); + + // Line 4 + + String rssi = data["rssi"]; + String battery_ok = data["battery_ok"]; + + String line4 = "batt: " + battery_ok + " rssi: " + rssi; + line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH); + + // Queue completed message + + if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) { + Log.error(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title); + free(message); + } else { + // Log.notice(F("[ WebUI ] Queued %s" CR), message->title); + } + break; + } +# endif +# ifdef ZsensorBME280 + case webUIHash("CLIMAtoMQTT"): { + // {"tempc":17.06,"tempf":62.708,"hum":50.0752,"pa":98876.14,"altim":205.8725,"altift":675.4348} + + // Line 1 + + strlcpy(message->line1, "bme280", WEBUI_TEXT_WIDTH); + + // Line 2 + + String line2 = ""; + if (data.containsKey("tempc")) { + char temp[5]; + float temperature_C = data["tempc"]; + + if (displayMetric) { + dtostrf(temperature_C, 3, 1, temp); + line2 = "temp: " + (String)temp + "°C "; + } else { + dtostrf(convertTemp_CtoF(temperature_C), 3, 1, temp); + line2 = "temp: " + (String)temp + "°F "; + } + } + line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH); + + // Line 3 + + String line3 = ""; + float humidity = data["hum"]; + if (data.containsKey("hum") && humidity <= 100 && humidity >= 0) { + char hum[5]; + dtostrf(humidity, 3, 1, hum); + line3 += "hum: " + (String)hum + "% "; + } + line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH); + + // Line 4 + + float pa = (int)data["pa"] / 100; + char pressure[6]; + + String line4 = ""; + if (displayMetric) { + dtostrf(pa, 3, 1, pressure); + line4 = "pressure: " + (String)pressure + " hPa"; + } else { + dtostrf(convert_hpa2inhg(pa), 3, 1, pressure); + line4 = "pressure: " + (String)pressure + " inHg"; + } + line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH); + + // Queue completed message + + if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) { + Log.error(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title); + free(message); + } else { + // Log.notice(F("[ WebUI ] Queued %s" CR), message->title); + } + break; + } +# endif +# ifdef ZgatewayBT + case webUIHash("BTtoMQTT"): { + // {"id":"AA:BB:CC:DD:EE:FF","mac_type":0,"adv_type":0,"name":"sps","manufacturerdata":"de071f1000b1612908","rssi":-70,"brand":"Inkbird","model":"T(H) Sensor","model_id":"IBS-TH1/TH2/P01B","type":"THBX","cidc":false,"acts":true,"tempc":20.14,"tempf":68.252,"hum":41.27,"batt":41} + + if (data["model_id"] != "MS-CDP" && data["model_id"] != "GAEN" && data["model_id"] != "APPLE_CONT" && data["model_id"] != "IBEACON") { + // Line 2, 3, 4 + String line2 = ""; + String line3 = ""; + String line4 = ""; + + // Properties + String properties[6] = {"", "", "", "", "", ""}; + int property = -1; + + if (data["type"] == "THB" || data["type"] == "THBX" || data["type"] == "PLANT" || data["type"] == "AIR" || data["type"] == "BATT" || data["type"] == "ACEL" || (data["type"] == "UNIQ" && data["model_id"] == "SDLS")) { + if (data.containsKey("tempc")) { + property++; + char temp[5]; + if (displayMetric) { + float temperature = data["tempc"]; + dtostrf(temperature, 3, 1, temp); + properties[property] = "temp: " + (String)temp + "°C "; + } else { + float temperature = data["tempf"]; + dtostrf(temperature, 3, 1, temp); + properties[property] = "temp: " + (String)temp + "°F "; + } + } + + if (data.containsKey("tempc2_dp")) { + property++; + char tempdp[5]; + if (displayMetric) { + float temperature = data["tempc2_dp"]; + dtostrf(temperature, 3, 1, tempdp); + properties[property] = "dewp: " + (String)tempdp + "°C "; + } else { + float temperature = data["tempf2_dp"]; + dtostrf(temperature, 3, 1, tempdp); + properties[property] = "dewp: " + (String)tempdp + "°F "; + } + } + + if (data.containsKey("extprobe")) { + property++; + properties[property] = " ext. probe"; + } + + if (data.containsKey("hum")) { + property++; + float humidity = data["hum"]; + char hum[5]; + + dtostrf(humidity, 3, 1, hum); + properties[property] = "hum: " + (String)hum + "% "; + } + + if (data.containsKey("pm25")) { + property++; + int pm25int = data["pm25"]; + char pm25[3]; + itoa(pm25int, pm25, 10); + if ((data.containsKey("pm10"))) { + properties[property] = "PM 2.5: " + (String)pm25 + " "; + + } else { + properties[property] = "pm2.5: " + (String)pm25 + "μg/m³ "; + } + } + + if (data.containsKey("pm10")) { + property++; + int pm10int = data["pm10"]; + char pm10[3]; + itoa(pm10int, pm10, 10); + if ((data.containsKey("pm25"))) { + properties[property] = "/ 10: " + (String)pm10 + "μg/m³ "; + + } else { + properties[property] = "pm10: " + (String)pm10 + "μg/m³ "; + } + } + + if (data.containsKey("for")) { + property++; + int formint = data["for"]; + char form[3]; + itoa(formint, form, 10); + properties[property] = "CH₂O: " + (String)form + "mg/m³ "; + } + + if (data.containsKey("co2")) { + property++; + int co2int = data["co2"]; + char co2[4]; + itoa(co2int, co2, 10); + properties[property] = "co2: " + (String)co2 + "ppm "; + } + + if (data.containsKey("moi")) { + property++; + int moiint = data["moi"]; + char moi[4]; + itoa(moiint, moi, 10); + properties[property] = "moi: " + (String)moi + "% "; + } + + if (data.containsKey("lux")) { + property++; + int luxint = data["lux"]; + char lux[5]; + itoa(luxint, lux, 10); + properties[property] = "lux: " + (String)lux + "lx "; + } + + if (data.containsKey("fer")) { + property++; + int ferint = data["fer"]; + char fer[7]; + itoa(ferint, fer, 10); + properties[property] = "fer: " + (String)fer + "µS/cm "; + } + + if (data.containsKey("pres")) { + property++; + int presint = data["pres"]; + char pres[4]; + itoa(presint, pres, 10); + properties[property] = "pres: " + (String)pres + "hPa "; + } + + if (data.containsKey("batt")) { + property++; + int battery = data["batt"]; + char batt[5]; + itoa(battery, batt, 10); + properties[property] = "batt: " + (String)batt + "% "; + } + + if (data.containsKey("shake")) { + property++; + int shakeint = data["shake"]; + char shake[3]; + itoa(shakeint, shake, 10); + properties[property] = "shake: " + (String)shake + " "; + } + + if (data.containsKey("volt")) { + property++; + float voltf = data["volt"]; + char volt[5]; + dtostrf(voltf, 3, 1, volt); + properties[property] = "volt: " + (String)volt + "V "; + } + + if (data.containsKey("wake")) { + property++; + String wakestr = data["wake"]; + properties[property] = "wake: " + wakestr + " "; + } + + } else if (data["type"] == "BBQ") { + String tempcstr = ""; + int j = 7; + if (data["model_id"] == "IBT-2X(S)") { + j = 3; + } else if (data["model_id"] == "IBT-4X(S/C)") { + j = 5; + } + + for (int i = 0; i < j; i++) { + if (i == 0) { + if (displayMetric) { + tempcstr = "tempc"; + } else { + tempcstr = "tempf"; + } + i++; + } else { + if (displayMetric) { + tempcstr = "tempc" + (String)i; + } else { + tempcstr = "tempf" + (String)i; + } + } + + if (data.containsKey(tempcstr)) { + char temp[5]; + float temperature = data[tempcstr]; + dtostrf(temperature, 3, 1, temp); + properties[i - 1] = "tp" + (String)i + ": " + (String)temp; + if (displayMetric) { + properties[i - 1] += "°C "; + } else { + properties[i - 1] += "°F "; + } + } else { + properties[i - 1] = "tp" + (String)i + ": " + "off "; + } + } + } else if (data["type"] == "BODY") { + if (data.containsKey("steps")) { + property++; + int stepsint = data["steps"]; + char steps[5]; + itoa(stepsint, steps, 10); + properties[property] = "steps: " + (String)steps + " "; + // next line + property++; + } + + if (data.containsKey("act_bpm")) { + property++; + int actbpmint = data["act_bpm"]; + char actbpm[3]; + itoa(actbpmint, actbpm, 10); + properties[property] = "activity bpm: " + (String)actbpm + " "; + } + + if (data.containsKey("bpm")) { + property++; + int bpmint = data["bpm"]; + char bpm[3]; + itoa(bpmint, bpm, 10); + properties[property] = "bpm: " + (String)bpm + " "; + } + } else if (data["type"] == "SCALE") { + if (data.containsKey("weighing_mode")) { + property++; + String mode = data["weighing_mode"]; + properties[property] = mode + " "; + // next line + property++; + } + + if (data.containsKey("weight")) { + property++; + float weightf = data["weight"]; + char weight[7]; + dtostrf(weightf, 3, 1, weight); + if (data.containsKey("unit")) { + String unit = data["unit"]; + properties[property] = "weight: " + (String)weight + unit + " "; + } else { + properties[property] = "weight: " + (String)weight; + } + // next line + property++; + } + + if (data.containsKey("impedance")) { + property++; + int impint = data["impedance"]; + char imp[3]; + itoa(impint, imp, 10); + properties[property] = "impedance: " + (String)imp + "ohm "; + } + } else if (data["type"] == "UNIQ") { + if (data["model_id"] == "M1017") { + if (data.containsKey("lvl_cm")) { + property++; + char lvl[5]; + if (displayMetric) { + float lvlf = data["lvl_cm"]; + dtostrf(lvlf, 3, 1, lvl); + properties[property] = "level: " + (String)lvl + "cm "; + } else { + float lvlf = data["lvl_in"]; + dtostrf(lvlf, 3, 1, lvl); + properties[property] = "level: " + (String)lvl + "\" "; + } + } + + if (data.containsKey("quality")) { + property++; + int qualint = data["quality"]; + char qual[3]; + itoa(qualint, qual, 10); + properties[property] = "qy: " + (String)qual + " "; + } + + if (data.containsKey("batt")) { + property++; + int battery = data["batt"]; + char batt[5]; + itoa(battery, batt, 10); + properties[property] = "batt: " + (String)batt + "% "; + } + } + } + + line2 = properties[0] + properties[1]; + line3 = properties[2] + properties[3]; + line4 = properties[4] + properties[5]; + + if (!(line2 == "" && line3 == "" && line4 == "")) { + // Titel + char* topic = strdup(topicori); + String heading = strtok(topic, "/"); + String line0 = heading + " " + data["id"].as().substring(9, 17); + line0.toCharArray(message->title, WEBUI_TEXT_WIDTH); + free(topic); + + // Line 1 + strlcpy(message->line1, data["model"], WEBUI_TEXT_WIDTH); + + line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH); + line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH); + line4.toCharArray(message->line4, WEBUI_TEXT_WIDTH); + + if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) { + Log.error(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title); + free(message); + } else { + Log.notice(F("[ WebUI ] Queued %s" CR), message->title); + } + } else { + WEBUI_TRACE_LOG(F("[ WebUI ] incomplete messaage %s" CR), topicori); + free(message); + } + + break; + } else { + WEBUI_TRACE_LOG(F("[ WebUI ] incorrect model_id %s" CR), topicori); + free(message); + break; + } + } +# endif +# ifdef ZsensorRN8209 + case webUIHash("RN8209toMQTT"): { + // {"volt":1073178,"current":0,"power":0} + + // Line 1 + + String line1 = ""; + if (data.containsKey("volt")) { + char volt[5]; + float voltage = data["volt"]; + dtostrf(voltage, 3, 1, volt); + line1 = "volt: " + (String)volt; + } + line1.toCharArray(message->line1, WEBUI_TEXT_WIDTH); + + // Line 2 + + String line2 = ""; + if (data.containsKey("current")) { + char curr[5]; + float current = data["current"]; + dtostrf(current, 3, 1, curr); + line2 = "current: " + (String)curr + " A"; + } + line2.toCharArray(message->line2, WEBUI_TEXT_WIDTH); + + // Line 3 + + String line3 = ""; + if (data.containsKey("power")) { + char pow[5]; + float power = data["power"]; + dtostrf(power, 3, 1, pow); + line3 = "power: " + (String)pow + " W"; + } + line3.toCharArray(message->line3, WEBUI_TEXT_WIDTH); + + // Queue completed message + + if (xQueueSend(webUIQueue, (void*)&message, 0) != pdTRUE) { + Log.error(F("[ WebUI ] webUIQueue full, discarding signal %s" CR), message->title); + free(message); + } else { + // Log.notice(F("[ WebUI ] Queued %s" CR), message->title); + } + break; + } +# endif + default: + Log.verbose(F("[ WebUI ] unhandled topic %s" CR), message->title); + free(message); + } + } else { + Log.error(F("[ WebUI ] insufficent memory " CR)); + } + } else { + Log.error(F("[ WebUI ] not initalized " CR)); + } +} + +/*------------------- Serial logging interceptor ----------------------*/ + +// This pattern was borrowed from HardwareSerial and modified to support the WebUI display + +SerialWeb WebLog(0); // Not sure about this, came from Hardwareserial +SerialWeb::SerialWeb(int x) { +} + +/* +Initialize WebUI oled display for use, and display OMG logo +*/ +void SerialWeb::begin() { + // WebUI.begin(); // User OMG serial support +} + +/* +Dummy virtual functions carried over from Serial +*/ +int SerialWeb::available(void) { +} + +/* +Dummy virtual functions carried over from Serial +*/ +int SerialWeb::peek(void) { +} + +/* +Dummy virtual functions carried over from Serial +*/ +int SerialWeb::read(void) { +} + +/* +Dummy virtual functions carried over from Serial +*/ +void SerialWeb::flush(void) { +} + +/* +Write line of text to the display with vertical scrolling of screen +*/ +size_t SerialWeb::write(const uint8_t* buffer, size_t size) { + // Default to Serial output if the display is not available + addLog(buffer, size); + return Serial.write(buffer, size); +} + +char line[ROW_LENGTH]; +int lineIndex = 0; +void addLog(const uint8_t* buffer, size_t size) { + for (int i = 0; i < size; i++) { + if (char(buffer[i]) == 10 | lineIndex > ROW_LENGTH - 2) { + if (char(buffer[i]) != 10) { + line[lineIndex++] = char(buffer[i]); + } + line[lineIndex++] = char(0); + AddLogData(1, (const char*)&line[0]); + lineIndex = 0; + } else { + line[lineIndex++] = char(buffer[i]); + } + } +} + +#endif \ No newline at end of file diff --git a/main/config_BT.h b/main/config_BT.h index f0d84c2..112a7f5 100644 --- a/main/config_BT.h +++ b/main/config_BT.h @@ -34,7 +34,7 @@ extern void launchBTDiscovery(bool overrideDiscovery); extern void stopProcessing(); extern void startProcessing(); extern void lowPowerESP32(); -extern void stateBTMeasures(bool); +extern String stateBTMeasures(bool); #ifdef ESP32 extern int btQueueBlocked; diff --git a/main/config_RF.h b/main/config_RF.h index ec02eea..7cfb8ba 100644 --- a/main/config_RF.h +++ b/main/config_RF.h @@ -246,8 +246,6 @@ bool validFrequency(float mhz) { int currentReceiver = -1; -extern void stateMeasures(); // Send a status message - # if !defined(ZgatewayRFM69) && !defined(ZactuatorSomfy) # if defined(ESP8266) || defined(ESP32) // Check if a receiver is available diff --git a/main/config_SSD1306.h b/main/config_SSD1306.h index 9a77cca..6f66229 100644 --- a/main/config_SSD1306.h +++ b/main/config_SSD1306.h @@ -39,6 +39,7 @@ #include #include "SSD1306Wire.h" +#include "config_WebUI.h" /*-------------------DEFINE LOG LEVEL----------------------*/ @@ -50,8 +51,14 @@ # define LOG_TO_OLED false // Default to not display log messages on display #endif -#ifndef JSON_TO_OLED -# define JSON_TO_OLED true // Default to displaying JSON messages on the display +#ifndef ZwebUI +# ifndef JSON_TO_OLED +# define JSON_TO_OLED true // Default to displaying JSON messages on the display +# endif +#else +# ifndef JSON_TO_OLED +# define JSON_TO_OLED false // If WebUI is NOT included within the build, the JSON message parser is not included within the build and the JSON messages are not available for display +# endif #endif #ifndef DISPLAY_PAGE_INTERVAL @@ -62,10 +69,6 @@ # define DISPLAY_IDLE_LOGO true // Display the OMG logo when idle #endif -#ifndef DISPLAY_METRIC -# define DISPLAY_METRIC true // Units used for display of sensor data -#endif - #ifndef DISPLAY_FLIP # define DISPLAY_FLIP true // Flip display orientation #endif @@ -119,30 +122,6 @@ void ssd1306Print(char*, char*, char*); void ssd1306Print(char*, char*); void ssd1306Print(char*); -#define pubOled(...) ssd1306PubPrint(__VA_ARGS__) -void ssd1306PubPrint(const char*, JsonObject&); - -// Structure for queueing OMG messages to the display - -/* -Structure for queueing OMG messages to the display. -Length of each line is OLED_TEXT_WIDTH -- title -- line1 -- line2 -- line3 -- line4 -*/ -struct displayQueueMessage { - char title[OLED_TEXT_WIDTH]; - char line1[OLED_TEXT_WIDTH]; - char line2[OLED_TEXT_WIDTH]; - char line3[OLED_TEXT_WIDTH]; - char line4[OLED_TEXT_WIDTH]; -}; - -displayQueueMessage* currentOledMessage; - /*-------------------End of Global Variables----------------------*/ // This pattern was borrowed from HardwareSerial and modified to support the ssd1306 display @@ -152,7 +131,7 @@ public: OledSerial(int); void begin(); void drawLogo(int xshift, int yshift); - boolean displayPage(displayQueueMessage*); + boolean displayPage(webUIQueueMessage*); SSD1306Wire* display; @@ -194,22 +173,4 @@ protected: extern OledSerial Oled; -/*------------------- Unit Conversion Functions ----------------------*/ - -#define convert_kmph2mph(kmph) (kmph * (1.0f / 1.609344f)) - -#define convert_mph2kmph(mph) (mph * 1.609344f) - -#define convert_mm2inch(mm) (mm * 0.039370f) - -#define convert_inch2mm(inch) (inch * 25.4f) - -#define convert_kpa2psi(kpa) (kpa * (1.0f / 6.89475729f)) - -#define convert_psi2kpa(psi) (psi * 6.89475729f) - -#define convert_hpa2inhg(hpa) (hpa * (1.0f / 33.8639f)) - -#define convert_inhg2hpa(inhg) (inhg * 33.8639f) - #endif diff --git a/main/config_WebContent.h b/main/config_WebContent.h new file mode 100644 index 0000000..7d68c9a --- /dev/null +++ b/main/config_WebContent.h @@ -0,0 +1,94 @@ +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a wifi or ethernet gateway between your 433mhz/infrared IR signal and a MQTT broker + Send and receiving command by MQTT + + This files enables to set your parameter for the DHT11/22 sensor + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#ifndef config_WebContent_h +#define config_WebContent_h + +/*------------------- ----------------------*/ + +// TODO: Create a script to generate these from WebPack + +#define body_footer_main_menu "

" +#define body_footer_config_menu "

"; +#define body_header "

%s

%s

" + +#define button_upgrade "

" + +// Configuration Menu + +#define configure_1 "

" +#define configure_2 "

" +#if defined(ZgatewayCloud) +# define configure_3 "

" +#else +# define configure_3 +#endif +#define configure_4 +#define configure_5 +#define configure_6 +#define configure_7 +#define configure_8 + +/*------------------- ----------------------*/ + +const char header_html[] = "%s"; + +const char style[] = ""; + +const char root_body[] = body_header "

" button_upgrade "

"; + +const char config_body[] = body_header "" configure_1 "" configure_2 "" configure_3 "" configure_4 "" configure_5 "" configure_6 "" configure_7 "" configure_8 "

" body_footer_main_menu; + +const char reset_body[] = body_header "
%s

Device will restart in a few seconds

" body_footer_main_menu; + +const char config_cloud_body[] = body_header "
 Cloud Configuration 



" body_footer_config_menu; + +const char token_body[] = body_header "div style='text-align:center;'>Link Cloud Account

Cloud was successfully linked

"; + +const char console_body[] = body_header "



" body_footer_main_menu; + +const char information_body[] = body_header "
" body_footer_main_menu; + +const char upgrade_body[] = body_header "
 Upgrade by web server 

OTA Url




 Upgrade by file upload 



" body_footer_main_menu; + +const char config_wifi_body[] = body_header "



 Wifi parameters 

WiFi Network ()


WiFi Network 2 ()


Hostname%s

CORS Domain


" body_footer_config_menu; + +// mqtt server, mqtt port, client id, mqtt username, mqtt password, topic, Full Topic + +const char config_mqtt_body[] = body_header "
 MQTT parameters 

Host ()

Port (1883)

Client

User " MQTT_USER "


Topic

Full Topic


" body_footer_config_menu; + +const char footer[] = ""; + +#endif \ No newline at end of file diff --git a/main/config_WebUI.h b/main/config_WebUI.h new file mode 100644 index 0000000..dbf6026 --- /dev/null +++ b/main/config_WebUI.h @@ -0,0 +1,161 @@ +/* + OpenMQTTGateway - ESP8266 or Arduino program for home automation + + Act as a wifi or ethernet gateway between your 433mhz/infrared IR signal and a MQTT broker + Send and receiving command by MQTT + + This files enables to set your parameter for the DHT11/22 sensor + + Copyright: (c)Florian ROBERT + + This file is part of OpenMQTTGateway. + + OpenMQTTGateway is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenMQTTGateway is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#ifndef config_WebUI_h +#define config_WebUI_h + +#include + +#define WEBUI_TEXT_WIDTH 128 + +/*------------------- Optional Compiler Directives ----------------------*/ + +#ifndef WEB_TEMPLATE_BUFFER_MAX_SIZE +# define WEB_TEMPLATE_BUFFER_MAX_SIZE 2000 +#endif + +#ifndef DISPLAY_METRIC +# define DISPLAY_METRIC true // Units used for display of sensor data +#endif + +#ifndef DISPLAY_WEBUI_INTERVAL +# define DISPLAY_WEBUI_INTERVAL 3 // Number of seconds between json message displays +#endif + +#ifdef WEBUI_DEVELOPMENT +# pragma message("[WebUI] Usage of SPIFFS for missing WebUI content enabled") +# define FILESYSTEM SPIFFS +# define WEBUI_TRACE_LOGGING +#endif + +#ifdef WEBUI_TRACE_LOGGING +# define WEBUI_TRACE_LOG(...) Log.trace(__VA_ARGS__) +#else +# define WEBUI_TRACE_LOG(...) +#endif + +/*------------------- End of Compiler Directives ----------------------*/ + +// Structure for queueing OMG messages to the display + +/* +Structure for queueing OMG messages to the display. +Length of each line is WEBUI_TEXT_WIDTH +- title +- line1 +- line2 +- line3 +- line4 +*/ + +struct webUIQueueMessage { + char title[WEBUI_TEXT_WIDTH]; + char line1[WEBUI_TEXT_WIDTH]; + char line2[WEBUI_TEXT_WIDTH]; + char line3[WEBUI_TEXT_WIDTH]; + char line4[WEBUI_TEXT_WIDTH]; +} webUIQueueMessage_t; + +/*------------------- Global Functions and Variables ----------------------*/ + +#ifdef ZwebUI +# define pubWebUI(...) webUIPubPrint(__VA_ARGS__) +void webUIPubPrint(const char*, JsonObject&); +#endif +void WebUISetup(); +void WebUILoop(); +void MQTTtoWebUI(char*, JsonObject&); + +String stateWebUIStatus(); + +webUIQueueMessage* currentWebUIMessage; + +/*------------------- End of Global Functions ----------------------*/ + +#define subjectMQTTtoWebUIset "/commands/MQTTtoWebUI/config" +#define subjectWebUItoMQTT "/WebUItoMQTT" + +/*------------------- Unit Conversion Functions ----------------------*/ + +#define convert_kmph2mph(kmph) (kmph * (1.0f / 1.609344f)) + +#define convert_mph2kmph(mph) (mph * 1.609344f) + +#define convert_mm2inch(mm) (mm * 0.039370f) + +#define convert_inch2mm(inch) (inch * 25.4f) + +#define convert_kpa2psi(kpa) (kpa * (1.0f / 6.89475729f)) + +#define convert_psi2kpa(psi) (psi * 6.89475729f) + +#define convert_hpa2inhg(hpa) (hpa * (1.0f / 33.8639f)) + +#define convert_inhg2hpa(inhg) (inhg * 33.8639f) + +/*------------------- Take over serial output and split to ----------------------*/ + +class SerialWeb : public Stream { +public: + SerialWeb(int); + void begin(); + + int available(void); // Dummy functions + int peek(void); // Dummy functions + int read(void); // Dummy functions + void flush(void); // Dummy functions + + inline size_t write(uint8_t x) { + return write(&x, 1); + } + + size_t write(const uint8_t* buffer, size_t size); + inline size_t write(const char* buffer, size_t size) { + return write((uint8_t*)buffer, size); + } + inline size_t write(const char* s) { + return write((uint8_t*)s, strlen(s)); + } + inline size_t write(unsigned long n) { + return write((uint8_t)n); + } + inline size_t write(long n) { + return write((uint8_t)n); + } + inline size_t write(unsigned int n) { + return write((uint8_t)n); + } + inline size_t write(int n) { + return write((uint8_t)n); + } + +protected: +}; + +extern SerialWeb WebLog; + +/*------------------- Take over serial output and split to ----------------------*/ + +#endif \ No newline at end of file diff --git a/main/main.ino b/main/main.ino index 5c5b4e5..6fe7e3b 100644 --- a/main/main.ino +++ b/main/main.ino @@ -70,6 +70,9 @@ struct GfSun2000Data {}; #endif // Modules config inclusion +#if defined(ZwebUI) && defined(ESP32) +# include "config_WebUI.h" +#endif #if defined(ZgatewayRF) || defined(ZgatewayRF2) || defined(ZgatewayPilight) || defined(ZactuatorSomfy) || defined(ZgatewayRTL_433) # include "config_RF.h" #endif @@ -396,6 +399,7 @@ void pub(const char* topicori, JsonObject& data) { #if jsonPublishing Log.trace(F("jsonPubl - ON" CR)); pubMQTT(topic, dataAsString.c_str()); + pubWebUI(topicori, data); #endif #if simplePublishing @@ -811,6 +815,11 @@ void setup() { setup_ethernet(); #endif +#if defined(ZwebUI) && defined(ESP32) + WebUISetup(); + modules.add(ZwebUI); +#endif + #if defined(ESP8266) || defined(ESP32) if (mqtt_secure) { eClient = new WiFiClientSecure; @@ -1184,6 +1193,7 @@ void setup_wifi() { # endif } Log.notice(F("WiFi ok with manual config credentials" CR)); + displayPrint("Wifi connected"); } #elif defined(ESP8266) || defined(ESP32) @@ -1610,6 +1620,9 @@ void loop() { # endif # ifdef ZdisplaySSD1306 stateSSD1306Display(); +# endif +# if defined(ZwebUI) && defined(ESP32) + stateWebUIStatus(); # endif } if (now > (timer_sys_checks + (TimeBetweenCheckingSYS * 1000)) || !timer_sys_checks) { @@ -1729,6 +1742,9 @@ void loop() { if (disc) launchRTL_433Discovery(publishDiscovery); # endif +#endif +#if defined(ZwebUI) && defined(ESP32) + WebUILoop(); #endif } else { // MQTT disconnected @@ -1827,7 +1843,7 @@ String UTCtimestamp() { #endif #if defined(ESP8266) || defined(ESP32) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1280__) -void stateMeasures() { +String stateMeasures() { StaticJsonDocument jsonBuffer; JsonObject SYSdata = jsonBuffer.to(); SYSdata["uptime"] = uptime(); @@ -1919,8 +1935,12 @@ void stateMeasures() { } # endif SYSdata["modules"] = modules; + String output; + serializeJson(SYSdata, output); + pub(subjectSYStoMQTT, SYSdata); pubOled(subjectSYStoMQTT, SYSdata); + return output; } #endif diff --git a/platformio.ini b/platformio.ini index 4b1b207..ab79495 100644 --- a/platformio.ini +++ b/platformio.ini @@ -187,6 +187,7 @@ build_flags = '-DMQTTsetMQTT' '-DMQTT_HTTPS_FW_UPDATE' ;'-DCORE_DEBUG_LEVEL=4' + '-DZwebUI="WebUI"' ; enable WebUI as a default for all ESP32 builds ( the module only enables for ESP32 based builds ) [com-arduino] lib_deps =