#include "ESPUI.h" #include "dataIndexHTML.h" #include "dataNormalizeCSS.h" #include "dataStyleCSS.h" #include "dataControlsJS.h" #include "dataGraphJS.h" #include "dataSliderJS.h" #include "dataTabbedcontentJS.h" #include "dataZeptoJS.h" #include #include uint16_t Control::idCounter = 0; // ################# Spiffs functions #if defined(ESP32) void listDir(const char *dirname, uint8_t levels) { if (ESPUI.verbosity) { Serial.printf("Listing directory: %s\n", dirname); } File root = LittleFS.open(dirname); if (!root) { if (ESPUI.verbosity) { Serial.println("Failed to open directory"); } return; } if (!root.isDirectory()) { if (ESPUI.verbosity) { Serial.println("Not a directory"); } return; } File file = root.openNextFile(); while (file) { if (file.isDirectory()) { if (ESPUI.verbosity) { Serial.print(" DIR : "); Serial.println(file.name()); } if (levels) { listDir(file.name(), levels - 1); } } else { if (ESPUI.verbosity) { Serial.print(" FILE: "); Serial.print(file.name()); Serial.print(" SIZE: "); Serial.println(file.size()); } } file = root.openNextFile(); } } #else void listDir(const char *dirname, uint8_t levels) { // ignoring levels for esp8266 Serial.printf("Listing directory: %s\n", dirname); String str = ""; Dir dir = LittleFS.openDir("/"); while (dir.next()) { Serial.print(" FILE: "); Serial.print(dir.fileName()); Serial.print(" SIZE: "); Serial.println(dir.fileSize()); } } #endif void ESPUIClass::list() { if (!LittleFS.begin()) { Serial.println("SPIFFS Mount Failed"); return; } listDir("/", 1); #if defined(ESP32) Serial.println(LittleFS.totalBytes()); Serial.println(LittleFS.usedBytes()); #else FSInfo fs_info; LittleFS.info(fs_info); Serial.println(fs_info.totalBytes); Serial.println(fs_info.usedBytes); #endif } void deleteFile(const char *path) { if (ESPUI.verbosity) { Serial.print(LittleFS.exists(path)); } if (!LittleFS.exists(path)) { if (ESPUI.verbosity) { Serial.printf("File: %s does not exist, not deleting\n", path); } return; } if (ESPUI.verbosity) { Serial.printf("Deleting file: %s\n", path); } if (LittleFS.remove(path)) { if (ESPUI.verbosity) { Serial.println("File deleted"); } } else { if (ESPUI.verbosity) { Serial.println("Delete failed"); } } } void writeFile(const char *path, const char *data) { if (ESPUI.verbosity) { Serial.printf("Writing file: %s\n", path); } File file = LittleFS.open(path, FILE_WRITE); if (!file) { if (ESPUI.verbosity) { Serial.println("Failed to open file for writing"); } return; } #if defined(ESP32) if (file.print(data)) { if (ESPUI.verbosity) { Serial.println("File written"); } } else { if (ESPUI.verbosity) { Serial.println("Write failed"); } } #else if (file.print(FPSTR(data))) { if (ESPUI.verbosity) { Serial.println("File written"); } } else { if (ESPUI.verbosity) { Serial.println("Write failed"); } } #endif file.close(); } // end Spiffs functions void ESPUIClass::prepareFileSystem() { // this function should only be used once if (this->verbosity) { Serial.println("About to prepare filesystem..."); } #if defined(ESP32) LittleFS.format(); if (!LittleFS.begin(true)) { if (this->verbosity) { Serial.println("SPIFFS Mount Failed"); } return; } if (this->verbosity) { listDir("/", 1); Serial.println("SPIFFS Mount ESP32 Done"); } #else LittleFS.format(); LittleFS.begin(); if (this->verbosity) { Serial.println("SPIFFS Mount ESP8266 Done"); } #endif deleteFile("/index.htm"); deleteFile("/css/style.css"); deleteFile("/css/normalize.css"); deleteFile("/js/zepto.min.js"); deleteFile("/js/controls.js"); deleteFile("/js/slider.js"); deleteFile("/js/graph.js"); deleteFile("/js/tabbedcontent.js"); if (this->verbosity) { Serial.println("Cleanup done"); } // Now write writeFile("/index.htm", HTML_INDEX); writeFile("/css/style.css", CSS_STYLE); writeFile("/css/normalize.css", CSS_NORMALIZE); writeFile("/js/zepto.min.js", JS_ZEPTO); writeFile("/js/controls.js", JS_CONTROLS); writeFile("/js/slider.js", JS_SLIDER); writeFile("/js/graph.js", JS_GRAPH); writeFile("/js/tabbedcontent.js", JS_TABBEDCONTENT); if (this->verbosity) { Serial.println("Done Initializing filesystem :-)"); } #if defined(ESP32) if (this->verbosity) { listDir("/", 1); } #endif LittleFS.end(); } // Handle Websockets Communication void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch (type) { case WS_EVT_DISCONNECT: { if (ESPUI.verbosity) { Serial.printf("Disconnected!\n"); } break; } case WS_EVT_PONG: { if (ESPUI.verbosity) { Serial.printf("Received PONG!\n"); } break; } case WS_EVT_ERROR: { if (ESPUI.verbosity) { Serial.printf("WebSocket Error!\n"); } break; } case WS_EVT_CONNECT: { if (ESPUI.verbosity) { Serial.print("Connected: "); Serial.println(client->id()); } ESPUI.jsonDom(client); if (ESPUI.verbosity) { Serial.println("JSON Data Sent to Client!"); } } break; case WS_EVT_DATA: { String msg = ""; msg.reserve(len + 1); for (size_t i = 0; i < len; i++) { msg += (char)data[i]; } uint16_t id = msg.substring(msg.lastIndexOf(':') + 1).toInt(); if (ESPUI.verbosity >= Verbosity::VerboseJSON) { Serial.print("WS rec: "); Serial.println(msg); Serial.print("WS recognised ID: "); Serial.println(id); } Control *c = ESPUI.getControl(id); if (c == nullptr) { if (ESPUI.verbosity) { Serial.print("No control found for ID "); Serial.println(id); } return; } if (c->callback == nullptr) { if (ESPUI.verbosity) { Serial.print("No callback found for ID "); Serial.println(id); } return; } if (msg.startsWith("bdown:")) { c->callback(c, B_DOWN); } else if (msg.startsWith("bup:")) { c->callback(c, B_UP); } else if (msg.startsWith("pfdown:")) { c->callback(c, P_FOR_DOWN); } else if (msg.startsWith("pfup:")) { c->callback(c, P_FOR_UP); } else if (msg.startsWith("pldown:")) { c->callback(c, P_LEFT_DOWN); } else if (msg.startsWith("plup:")) { c->callback(c, P_LEFT_UP); } else if (msg.startsWith("prdown:")) { c->callback(c, P_RIGHT_DOWN); } else if (msg.startsWith("prup:")) { c->callback(c, P_RIGHT_UP); } else if (msg.startsWith("pbdown:")) { c->callback(c, P_BACK_DOWN); } else if (msg.startsWith("pbup:")) { c->callback(c, P_BACK_UP); } else if (msg.startsWith("pcdown:")) { c->callback(c, P_CENTER_DOWN); } else if (msg.startsWith("pcup:")) { c->callback(c, P_CENTER_UP); } else if (msg.startsWith("sactive:")) { c->value = "1"; ESPUI.updateControl(c, client->id()); c->callback(c, S_ACTIVE); } else if (msg.startsWith("sinactive:")) { c->value = "0"; ESPUI.updateControl(c, client->id()); c->callback(c, S_INACTIVE); } else if (msg.startsWith("slvalue:")) { c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); ESPUI.updateControl(c, client->id()); c->callback(c, SL_VALUE); } else if (msg.startsWith("nvalue:")) { c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); ESPUI.updateControl(c, client->id()); c->callback(c, N_VALUE); } else if (msg.startsWith("tvalue:")) { c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); ESPUI.updateControl(c, client->id()); c->callback(c, T_VALUE); } else if (msg.startsWith("svalue:")) { c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':')); ESPUI.updateControl(c, client->id()); c->callback(c, S_VALUE); } else { if (ESPUI.verbosity) { Serial.println("Malformated message from the websocket"); } } } break; default: break; } } uint16_t ESPUIClass::addControl(ControlType type, const char *label, String value, ControlColor color, uint16_t parentControl, void (*callback)(Control *, int)) { Control *control = new Control(type, label, callback, value, color, parentControl); if (this->controls == nullptr) { this->controls = control; } else { Control *iterator = this->controls; while (iterator->next != nullptr) { iterator = iterator->next; } iterator->next = control; } return control->id; } bool ESPUIClass::removeControl(uint16_t id, bool force_reload_ui) { if (nullptr == this->controls) return false; Control *it = this->controls; if (id == it->id) { this->controls = it->next; delete it; if (force_reload_ui) { jsonReload(); } else { jsonDom(); } return true; } Control *it_next = it->next; while (nullptr != it_next && id != it_next->id) { it = it_next; it_next = it_next->next; } if (nullptr != it_next) { it->next = it_next->next; delete it_next; if (force_reload_ui) { jsonReload(); } else { jsonDom(); } return true; } return false; } uint16_t ESPUIClass::label(const char *label, ControlColor color, String value) { return addControl(ControlType::Label, label, value, color); } uint16_t ESPUIClass::graph(const char *label, ControlColor color) { return addControl(ControlType::Graph, label, "", color); } uint16_t ESPUIClass::slider(const char *label, void (*callback)(Control *, int), ControlColor color, int value, int min, int max) { uint16_t sliderId = addControl(ControlType::Slider, label, String(value), color, Control::noParent, callback); addControl(ControlType::Min, label, String(min), ControlColor::None, sliderId); addControl(ControlType::Max, label, String(max), ControlColor::None, sliderId); return sliderId; } uint16_t ESPUIClass::button(const char *label, void (*callback)(Control *, int), ControlColor color, String value) { return addControl(ControlType::Button, label, value, color, Control::noParent, callback); } uint16_t ESPUIClass::switcher(const char *label, void (*callback)(Control *, int), ControlColor color, bool startState) { return addControl(ControlType::Switcher, label, startState ? "1" : "0", color, Control::noParent, callback); } uint16_t ESPUIClass::pad(const char *label, void (*callback)(Control *, int), ControlColor color) { return addControl(ControlType::Pad, label, "", color, Control::noParent, callback); } uint16_t ESPUIClass::padWithCenter(const char *label, void (*callback)(Control *, int), ControlColor color) { return addControl(ControlType::PadWithCenter, label, "", color, Control::noParent, callback); } uint16_t ESPUIClass::number(const char *label, void (*callback)(Control *, int), ControlColor color, int number, int min, int max) { uint16_t numberId = addControl(ControlType::Number, label, String(number), color, Control::noParent, callback); addControl(ControlType::Min, label, String(min), ControlColor::None, numberId); addControl(ControlType::Max, label, String(max), ControlColor::None, numberId); return numberId; } uint16_t ESPUIClass::gauge(const char *label, ControlColor color, int number, int min, int max) { uint16_t numberId = addControl(ControlType::Gauge, label, String(number), color, Control::noParent); addControl(ControlType::Min, label, String(min), ControlColor::None, numberId); addControl(ControlType::Max, label, String(max), ControlColor::None, numberId); return numberId; } uint16_t ESPUIClass::accelerometer(const char *label, void (*callback)(Control *, int), ControlColor color) { return addControl(ControlType::Accel, label, "", color, Control::noParent, callback); } uint16_t ESPUIClass::text(const char *label, void (*callback)(Control *, int), ControlColor color, String value) { return addControl(ControlType::Text, label, value, color, Control::noParent, callback); } Control *ESPUIClass::getControl(uint16_t id) { Control *control = this->controls; while (control != nullptr) { if (control->id == id) { return control; } control = control->next; } return nullptr; } void ESPUIClass::updateControl(Control *control, int clientId) { if (!control) { return; } String json; DynamicJsonDocument document(jsonUpdateDocumentSize); JsonObject root = document.to(); root["type"] = (int)control->type + ControlType::UpdateOffset; root["value"] = control->value; root["id"] = control->id; root["color"] = (int)control->color; serializeJson(document, json); if (this->verbosity >= Verbosity::VerboseJSON) { Serial.println(json); } if (clientId < 0) { this->ws->textAll(json); return; } // This is a hacky workaround because ESPAsyncWebServer does not have a // function like this and it's clients array is private int tryId = 0; for (int count = 0; count < this->ws->count();) { if (this->ws->hasClient(tryId)) { if (clientId != tryId) { this->ws->client(tryId)->text(json); if (this->verbosity >= Verbosity::VerboseJSON) { Serial.println(json); } } count++; } tryId++; } } void ESPUIClass::updateControl(uint16_t id, int clientId) { Control *control = getControl(id); if (!control) { if (this->verbosity) { Serial.println(String("Error: There is no control with ID ") + String(id)); } return; } updateControl(control, clientId); } void ESPUIClass::updateControlValue(Control *control, String value, int clientId) { if (!control) { return; } control->value = value; updateControl(control, clientId); } void ESPUIClass::updateControlValue(uint16_t id, String value, int clientId) { Control *control = getControl(id); if (!control) { if (this->verbosity) { Serial.println(String("Error: There is no control with ID ") + String(id)); } return; } updateControlValue(control, value, clientId); } void ESPUIClass::print(uint16_t id, String value) { updateControlValue(id, value); } void ESPUIClass::updateLabel(uint16_t id, String value) { updateControlValue(id, value); } void ESPUIClass::updateSlider(uint16_t id, int nValue, int clientId) { updateControlValue(id, String(nValue), clientId); } void ESPUIClass::updateSwitcher(uint16_t id, bool nValue, int clientId) { updateControlValue(id, String(nValue ? "1" : "0"), clientId); } void ESPUIClass::updateNumber(uint16_t id, int number, int clientId) { updateControlValue(id, String(number), clientId); } void ESPUIClass::updateText(uint16_t id, String text, int clientId) { updateControlValue(id, text, clientId); } void ESPUIClass::updateSelect(uint16_t id, String text, int clientId) { updateControlValue(id, text, clientId); } void ESPUIClass::updateGauge(uint16_t id, int number, int clientId) { updateControlValue(id, String(number), clientId); } void ESPUIClass::clearGraph(uint16_t id, int clientId) {} void ESPUIClass::addGraphPoint(uint16_t id, int nValue, int clientId) { Control *control = getControl(id); if (!control) { return; } String json; DynamicJsonDocument document(jsonUpdateDocumentSize); JsonObject root = document.to(); root["type"] = (int)ControlType::GraphPoint; root["value"] = nValue; root["id"] = control->id; serializeJson(document, json); if (this->verbosity >= Verbosity::VerboseJSON) { Serial.println(json); } if (clientId < 0) { this->ws->textAll(json); return; } // This is a hacky workaround because ESPAsyncWebServer does not have a // function like this and it's clients array is private int tryId = 0; for (int count = 0; count < this->ws->count();) { if (this->ws->hasClient(tryId)) { if (clientId != tryId) { this->ws->client(tryId)->text(json); if (this->verbosity >= Verbosity::VerboseJSON) { Serial.println(json); } } count++; } tryId++; } } /* Convert & Transfer Arduino elements to JSON elements Initially this function used to send the control element data individually. Due to a change in the ESPAsyncWebserver library this had top be changed to be sent as one blob at the beginning. Therefore a new type is used as well */ void ESPUIClass::jsonDom(AsyncWebSocketClient *client) { String json; DynamicJsonDocument document(jsonInitialDocumentSize); document["type"] = (int)UI_INITIAL_GUI; document["sliderContinuous"] = sliderContinuous; JsonArray items = document.createNestedArray("controls"); Control *control = this->controls; JsonObject titleItem = items.createNestedObject(); titleItem["type"] = (int)UI_TITLE; titleItem["label"] = ui_title; while (control != nullptr) { JsonObject item = items.createNestedObject(); item["id"] = String(control->id); item["type"] = (int)control->type; item["label"] = control->label; item["value"] = String(control->value); item["color"] = (int)control->color; if (control->parentControl != Control::noParent) { item["parentControl"] = String(control->parentControl); } // special case for selects: to preselect an option, you have to add // "selected" to