diff --git a/src/hasp.cpp b/src/hasp.cpp new file mode 100644 index 00000000..3036ee31 --- /dev/null +++ b/src/hasp.cpp @@ -0,0 +1,1371 @@ +/********************* + * INCLUDES + *********************/ +#include +#include "ArduinoJson.h" + +#include "lvgl.h" +#include "lv_conf.h" +#include "lv_theme_hasp.h" +#include "lv_objx/lv_roller.h" + +#include "../include/hasp_conf.h" + +#if LV_USE_HASP_SPIFFS +#ifdef ESP32 +//#include "lv_zifont.h" +#include "SPIFFS.h" +#endif +#include "lv_zifont.h" +#include // Include the SPIFFS library +#endif + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_config.h" +#include "hasp_mqtt.h" +#include "hasp_gui.h" +#include "hasp_tft.h" +#include "hasp.h" + +//#if LV_USE_HASP + +/********************* + * DEFINES + *********************/ + +uint8_t haspStartPage = 0; +uint8_t haspThemeId = 0; +uint16_t haspThemeHue = 200; +String haspPagesPath; +String haspZiFontPath; + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * STATIC PROTOTYPES + **********************/ +static void keyboard_event_cb(lv_obj_t * keyboard, lv_event_t event); +#if LV_USE_ANIMATION +static void kb_hide_anim_end(lv_anim_t * a); +#endif + +void hasp_background(uint16_t pageid, uint16_t imageid); + +/********************** + * STATIC VARIABLES + **********************/ +static lv_style_t style_mbox_bg; /*Black bg. style with opacity*/ + +#if LV_DEMO_WALLPAPER +LV_IMG_DECLARE(img_bubble_pattern) +#endif + +/* +LV_IMG_DECLARE(xmass) + +LV_IMG_DECLARE(frame00) +LV_IMG_DECLARE(frame02) +LV_IMG_DECLARE(frame04) +LV_IMG_DECLARE(frame06) +LV_IMG_DECLARE(frame08) +LV_IMG_DECLARE(frame10) +LV_IMG_DECLARE(frame12) +LV_IMG_DECLARE(frame14) +*/ + +/* +static const char * btnm_map1[] = {" ", "\n", " ", "\n", " ", "\n", " ", "\n", "P1", "P2", "P3", ""}; + +static const char * btnm_map2[] = {"0", "1", "\n", "2", "3", "\n", "4", "5", + "\n", "6", "7", "\n", "P1", "P2", "P3", ""}; +*/ + +#ifdef ESP8266 +lv_obj_t * pages[4]; +// lv_style_t styles[6]; +#else +lv_obj_t * pages[12]; +// lv_style_t styles[20]; +#endif +uint16_t current_page = 0; +// uint16_t current_style = 0; + +/********************** + * MACROS + **********************/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ +void haspLoadPage(); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Get Page Object by PageID + */ +lv_obj_t * get_page(uint8_t pageid) +{ + if(pageid == 254) return lv_layer_top(); + if(pageid == 255) return lv_layer_sys(); + if(pageid >= sizeof pages / sizeof *pages) return NULL; + return pages[pageid]; +} +bool get_page_id(lv_obj_t * obj, uint8_t * pageid) +{ + lv_obj_t * page = lv_obj_get_screen(obj); + + if(!page) return false; + + if(page == lv_layer_top()) { + *pageid = 254; + return true; + } + if(page == lv_layer_sys()) { + *pageid = 255; + return true; + } + + for(uint8_t i = 0; i < sizeof pages / sizeof *pages; i++) { + if(page == pages[i]) { + *pageid = i; + return true; + } + } + return false; +} + +lv_obj_t * FindObjFromId(lv_obj_t * parent, uint8_t objid) +{ + lv_obj_t * child; + child = lv_obj_get_child(parent, NULL); + while(child) { + if(child->user_data && (lv_obj_user_data_t)objid == child->user_data) return child; // object found + + /* check grandchildren */ + // if(lv_obj_count_children(child) > 0) // tells the number of children on an object + //{ + lv_obj_t * grandchild = FindObjFromId(child, objid); + if(grandchild) return grandchild; + //} + + /* next sibling */ + child = lv_obj_get_child(parent, child); + } + return NULL; +} +lv_obj_t * FindObjFromId(uint8_t pageid, uint8_t objid) +{ + return FindObjFromId(get_page(pageid), objid); +} + +bool FindIdFromObj(lv_obj_t * obj, uint8_t * pageid, lv_obj_user_data_t * objid) +{ + if(!get_page_id(obj, pageid)) return false; + if(!(obj->user_data > 0)) return false; + memcpy(objid, &obj->user_data, sizeof(lv_obj_user_data_t)); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void haspGetAttr(String hmiAttribute) +{ // Get the value of a Nextion component attribute + // This will only send the command to the panel requesting the attribute, the actual + // return of that value will be handled by nextionProcessInput and placed into mqttGetSubtopic + /*Serial1.print("get " + hmiAttribute); + Serial1.write(nextionSuffix, sizeof(nextionSuffix));*/ + debugPrintln(String(F("HMI OUT: 'get ")) + hmiAttribute + "'"); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void haspProcessInput() +{ +#if LV_USE_HASP_MQTT > 0 + // mqttSend(topic, value); +#endif +} + +void haspSendNewEvent(lv_obj_t * obj, uint8_t val) +{ + uint8_t pageid; + uint8_t objid; + + if(FindIdFromObj(obj, &pageid, &objid)) { + // char buffer[40]; + // sprintf_P(buffer, PSTR("HASP: Send p[%u].b[%u].event=%d"), pageid, objid, val); + // debugPrintln(buffer); + +#if LV_USE_HASP_MQTT > 0 + mqttSendNewEvent(pageid, objid, val); +#endif + } +} + +void haspSendNewValue(lv_obj_t * obj, int32_t val) +{ + uint8_t pageid; + uint8_t objid; + + if(FindIdFromObj(obj, &pageid, &objid)) { + // char buffer[40]; + // sprintf_P(buffer, PSTR("HASP: Send p[%u].b[%u].val=%d"), pageid, objid, val); + // debugPrintln(buffer); + +#if LV_USE_HASP_MQTT > 0 + mqttSendNewValue(pageid, objid, val); +#endif + } +} + +void haspSendNewValue(lv_obj_t * obj, String txt) +{ + uint8_t pageid; + uint8_t objid; + + if(FindIdFromObj(obj, &pageid, &objid)) { + // char buffer[40]; + // sprintf_P(buffer, PSTR("HASP: Send p[%u].b[%u].txt='%s'"), pageid, objid, txt.c_str()); + // debugPrintln(buffer); + +#if LV_USE_HASP_MQTT > 0 + mqttSendNewValue(pageid, objid, txt); +#endif + } +} + +void haspSendNewValue(lv_obj_t * obj, const char * txt) +{ + uint8_t pageid; + uint8_t objid; + + if(FindIdFromObj(obj, &pageid, &objid)) { + // char buffer[40]; + // sprintf_P(buffer, PSTR("HASP: Send p[%u].b[%u].txt='%s'"), pageid, objid, txt); + // debugPrintln(buffer); + +#if LV_USE_HASP_MQTT > 0 + mqttSendNewValue(pageid, objid, txt); +#endif + } +} + +int32_t get_cpicker_value(lv_obj_t * obj) +{ + lv_color16_t c16; + c16.full = lv_color_to16(lv_cpicker_get_color(obj)); + return (int32_t)c16.full; +} + +void haspSendNewValue(lv_obj_t * obj, int16_t val) +{ + haspSendNewValue(obj, (int32_t)val); +} + +void haspSendNewValue(lv_obj_t * obj, lv_color_t color) +{ + haspSendNewValue(obj, get_cpicker_value(obj)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void haspSendCmd(String nextionCmd) +{ // Send a raw command to the Nextion panel + /*Serial1.print(utf8ascii(nextionCmd)); + Serial1.write(nextionSuffix, sizeof(nextionSuffix)); + debugPrintln(String(F("HMI OUT: ")) + nextionCmd);*/ +} + +void haspSetLabelText(String value) +{ + uint8_t pageid = 2; + lv_obj_t * page = get_page(pageid); + if(!page) { + errorPrintln(F("HASP: %sPage not defined")); + return; + } + + lv_obj_t * button = lv_obj_get_child_back(page, NULL); + if(button) { + lv_obj_t * label = lv_obj_get_child_back(button, NULL); + debugPrintln(String(F("HASP: Setting value to ")) + value); + // lv_label_set_text(label, value.c_str()); + } else { + warningPrintln(F("HASP: %shaspSetLabelText NULL Pointer encountered")); + } +} + +bool check_obj_type(const char * lvobjtype, lv_hasp_obj_type_t haspobjtype) +{ + switch(haspobjtype) { + case LV_HASP_BUTTON: + return (strcmp_P(lvobjtype, PSTR("lv_btn")) == 0); + case LV_HASP_LABEL: + return (strcmp_P(lvobjtype, PSTR("lv_label")) == 0); + case LV_HASP_CHECKBOX: + return (strcmp_P(lvobjtype, PSTR("lv_cb")) == 0); + case LV_HASP_DDLIST: + return (strcmp_P(lvobjtype, PSTR("lv_ddlist")) == 0); + case LV_HASP_CPICKER: + return (strcmp_P(lvobjtype, PSTR("lv_cpicker")) == 0); + case LV_HASP_PRELOADER: + return (strcmp_P(lvobjtype, PSTR("lv_preloader")) == 0); + case LV_HASP_SLIDER: + return (strcmp_P(lvobjtype, PSTR("lv_slider")) == 0); + case LV_HASP_GAUGE: + return (strcmp_P(lvobjtype, PSTR("lv_gauge")) == 0); + case LV_HASP_BAR: + return (strcmp_P(lvobjtype, PSTR("lv_bar")) == 0); + case LV_HASP_LMETER: + return (strcmp_P(lvobjtype, PSTR("lv_lmeter")) == 0); + case LV_HASP_ROLLER: + return (strcmp_P(lvobjtype, PSTR("lv_roller")) == 0); + case LV_HASP_SWITCH: + return (strcmp_P(lvobjtype, PSTR("lv_sw")) == 0); + case LV_HASP_LED: + return (strcmp_P(lvobjtype, PSTR("lv_led")) == 0); + default: + return false; + } +} + +bool haspGetObjAttribute(lv_obj_t * obj, String strAttr, std::string & strPayload) +{ + if(!obj) return false; + uint16_t val = 0; + + switch(strAttr.length()) { + case 2: + if(strAttr == F(".x")) { + val = lv_obj_get_x(obj); + } else if(strAttr == F(".y")) { + val = lv_obj_get_y(obj); + } else if(strAttr == F(".w")) { + val = lv_obj_get_width(obj); + } else if(strAttr == F(".h")) { + val = lv_obj_get_height(obj); + } else { + return false; + } + strPayload = String(val).c_str(); + return true; + case 3: + if(strAttr == F(".en")) { + strPayload = String(lv_obj_get_click(obj)).c_str(); + return true; + } else { + return false; + } + return false; + case 4: + if(strAttr == F(".vis")) { + strPayload = String(!lv_obj_get_hidden(obj)).c_str(); + return true; + } else { + /* .txt and .val depend on objecttype */ + lv_obj_type_t list; + lv_obj_get_type(obj, &list); + + if(strAttr == F(".txt")) { + if(check_obj_type(list.type[0], LV_HASP_LABEL)) { + strPayload = lv_label_get_text(obj); + return true; + } + if(check_obj_type(list.type[0], LV_HASP_CHECKBOX)) { + strPayload = lv_cb_get_text(obj); + return true; + } + if(check_obj_type(list.type[0], LV_HASP_DDLIST)) { + char buffer[128]; + lv_ddlist_get_selected_str(obj, buffer, sizeof(buffer)); + strPayload = String(buffer).c_str(); + return true; + } + if(check_obj_type(list.type[0], LV_HASP_ROLLER)) { + char buffer[128]; + lv_roller_get_selected_str(obj, buffer, sizeof(buffer)); + strPayload = String(buffer).c_str(); + return true; + } + return false; + } + + if(strAttr == F(".val")) { + if(check_obj_type(list.type[0], LV_HASP_PRELOADER)) return false; + + if(check_obj_type(list.type[0], LV_HASP_BUTTON)) + if(lv_btn_get_state(obj) == LV_BTN_STATE_TGL_PR || + lv_btn_get_state(obj) == LV_BTN_STATE_TGL_REL) + strPayload = "1"; // It's toggled + else + strPayload = "0"; // Normal btn has no toggle state + + if(check_obj_type(list.type[0], LV_HASP_SLIDER)) + strPayload = String(lv_slider_get_value(obj)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_GAUGE)) + strPayload = String(lv_gauge_get_value(obj, 0)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_BAR)) strPayload = String(lv_bar_get_value(obj)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_LMETER)) + strPayload = String(lv_lmeter_get_value(obj)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_CPICKER)) + strPayload = String(get_cpicker_value(obj)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_CHECKBOX)) + strPayload = String(!lv_cb_is_checked(obj) ? 0 : 1).c_str(); + if(check_obj_type(list.type[0], LV_HASP_DDLIST)) + strPayload = String(lv_ddlist_get_selected(obj)).c_str(); + if(check_obj_type(list.type[0], LV_HASP_ROLLER)) + strPayload = String(lv_roller_get_selected(obj)).c_str(); + + if(check_obj_type(list.type[0], LV_HASP_LED)) + strPayload = String(lv_led_get_bright(obj) != 255 ? 0 : 1).c_str(); + + if(check_obj_type(list.type[0], LV_HASP_SWITCH)) strPayload = String(lv_sw_get_state(obj)).c_str(); + + return true; + } + } + break; + case 8: + if(strAttr == F(".opacity")) { + strPayload = String(!lv_obj_get_opa_scale_enable(obj) ? 100 : lv_obj_get_opa_scale(obj)).c_str(); + + } else if(strAttr == F(".options")) { + /* options depend on objecttype */ + lv_obj_type_t list; + lv_obj_get_type(obj, &list); + + if(check_obj_type(list.type[0], LV_HASP_DDLIST)) { + strPayload = lv_ddlist_get_options(obj); + return true; + } + if(check_obj_type(list.type[0], LV_HASP_ROLLER)) { + strPayload = lv_roller_get_options(obj); + return true; + } + return false; + } + break; + default: + errorPrintln(F("HASP: %sUnknown property")); + return false; + } + return false; +} + +void haspSetAttr(String strTopic, String strPayload) +{} + +void haspSetObjAttribute(lv_obj_t * obj, String strAttr, String strPayload) +{ + if(!obj) return; + uint16_t val = (uint16_t)strPayload.toInt(); + + switch(strAttr.length()) { + case 2: + if(strAttr == F(".x")) { + lv_obj_set_x(obj, val); + } else if(strAttr == F(".y")) { + lv_obj_set_y(obj, val); + } else if(strAttr == F(".w")) { + lv_obj_set_width(obj, val); + } else if(strAttr == F(".h")) { + lv_obj_set_height(obj, val); + } else { + return; + } + break; + case 3: + if(strAttr == F(".en")) { + lv_obj_set_click(obj, val != 0); + } else { + return; + } + break; + case 4: + if(strAttr == F(".vis")) { + lv_obj_set_hidden(obj, val == 0); + } else { + /* .txt and .val depend on objecttype */ + lv_obj_type_t list; + lv_obj_get_type(obj, &list); + + if(strAttr == F(".txt")) { // In order of likelihood to occur + if(check_obj_type(list.type[0], LV_HASP_LABEL)) + lv_label_set_text(obj, strPayload.c_str()); + else if(check_obj_type(list.type[0], LV_HASP_CHECKBOX)) + lv_cb_set_text(obj, strPayload.c_str()); + else if(check_obj_type(list.type[0], LV_HASP_DDLIST)) + lv_ddlist_set_options(obj, strPayload.c_str()); + else if(check_obj_type(list.type[0], LV_HASP_ROLLER)) { + lv_roller_ext_t * ext = (lv_roller_ext_t *)lv_obj_get_ext_attr(obj); + lv_roller_set_options(obj, strPayload.c_str(), ext->mode); + } + return; + } + + if(strAttr == F(".val")) { // In order of likelihood to occur + int16_t intval = (int16_t)strPayload.toInt(); + + if(check_obj_type(list.type[0], LV_HASP_BUTTON)) { + if(lv_btn_get_toggle(obj)) + lv_btn_set_state(obj, val == 0 ? LV_BTN_STATE_REL : LV_BTN_STATE_TGL_REL); + + } else if(check_obj_type(list.type[0], LV_HASP_CHECKBOX)) + lv_cb_set_checked(obj, val != 0); + else if(check_obj_type(list.type[0], LV_HASP_SLIDER)) + lv_slider_set_value(obj, intval, LV_ANIM_ON); + + else if(check_obj_type(list.type[0], LV_HASP_SWITCH)) + val == 0 ? lv_sw_off(obj, LV_ANIM_ON) : lv_sw_on(obj, LV_ANIM_ON); + else if(check_obj_type(list.type[0], LV_HASP_LED)) + val == 0 ? lv_led_off(obj) : lv_led_on(obj); + else if(check_obj_type(list.type[0], LV_HASP_GAUGE)) + lv_gauge_set_value(obj, 0, intval); + else if(check_obj_type(list.type[0], LV_HASP_DDLIST)) + lv_ddlist_set_selected(obj, val); + else if(check_obj_type(list.type[0], LV_HASP_ROLLER)) + lv_roller_set_selected(obj, val, LV_ANIM_ON); + else if(check_obj_type(list.type[0], LV_HASP_BAR)) + lv_bar_set_value(obj, intval, LV_ANIM_ON); + else if(check_obj_type(list.type[0], LV_HASP_LMETER)) + lv_lmeter_set_value(obj, intval); + else if(check_obj_type(list.type[0], LV_HASP_CPICKER)) + lv_lmeter_set_value(obj, (uint8_t)val); + return; + } + } + break; + case 8: + if(strAttr == F(".opacity")) { + lv_obj_set_opa_scale_enable(obj, val < 100); + lv_obj_set_opa_scale(obj, val < 100 ? val : 100); + } + break; + default: + errorPrintln(F("HASP: %sUnknown property")); + } +} + +void haspProcessAttribute(String strTopic, String strPayload) +{ + if(strTopic.startsWith("p[")) { + String strPageId = strTopic.substring(2, strTopic.indexOf("]")); + String strTemp = strTopic.substring(strTopic.indexOf("]") + 1, strTopic.length()); + if(strTemp.startsWith(".b[")) { + String strObjId = strTemp.substring(3, strTemp.indexOf("]")); + String strAttr = strTemp.substring(strTemp.indexOf("]") + 1, strTemp.length()); + debugPrintln(strPageId + " && " + strObjId + " && " + strAttr); + + int pageid = strPageId.toInt(); + int objid = strObjId.toInt(); + + if(pageid >= 0 && pageid <= 255 && objid > 0 && objid <= 255) { + lv_obj_t * obj = FindObjFromId((uint8_t)pageid, (uint8_t)objid); + if(obj) { + if(strPayload != "") + haspSetObjAttribute(obj, strAttr, strPayload); + else { + /* publish the change */ + std::string strValue = ""; + if(haspGetObjAttribute(obj, strAttr, strValue)) { + mqttSendNewValue(pageid, objid, String(strValue.c_str())); + } else { + warningPrintln(String(F("HASP: %sUnknown property: ")) + strAttr); + } + } // payload + } // obj + } // valid page + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/* +static int16_t get_obj_id(lv_obj_t * obj) +{ + lv_obj_t * page = lv_scr_act(); + int16_t id = -1; + lv_obj_t * thisobj = NULL; + while(obj != thisobj && id < 256) { + thisobj = lv_obj_get_child_back(page, thisobj); + id++; + } + if(obj != thisobj) { + return -1; + } else { + return id; + } +} + +static int16_t get_obj(uint8_t pageid, uint8_t objid, lv_obj_t * obj) +{ + lv_obj_t * page = lv_scr_act(); + int16_t id = -1; + lv_obj_t * thisobj = NULL; + while(obj != thisobj && id < 256) { + thisobj = lv_obj_get_child_back(page, thisobj); + id++; + } + if(obj != thisobj) { + return -1; + } else { + return id; + } +} +*/ + +/** + * Connection lost GUI + */ + +void haspDisconnect() +{ + /* Create a dark plain style for a message box's background (modal)*/ + lv_style_copy(&style_mbox_bg, &lv_style_plain); + style_mbox_bg.body.main_color = LV_COLOR_BLACK; + style_mbox_bg.body.grad_color = LV_COLOR_BLACK; + style_mbox_bg.body.opa = LV_OPA_50; + + lv_obj_set_style(lv_disp_get_layer_sys(NULL), &style_mbox_bg); + lv_obj_set_click(lv_disp_get_layer_sys(NULL), true); + /* + lv_obj_t * obj = lv_obj_get_child(lv_disp_get_layer_sys(NULL), NULL); + lv_obj_set_hidden(obj, false); + obj = lv_obj_get_child(lv_disp_get_layer_sys(NULL), obj); + lv_obj_set_hidden(obj, false);*/ +} + +void haspReconnect() +{ + /*Revert the top layer to not block*/ + lv_obj_set_style(lv_disp_get_layer_sys(NULL), &lv_style_transp); + lv_obj_set_click(lv_disp_get_layer_sys(NULL), false); + /* + lv_obj_t * obj = lv_obj_get_child(lv_disp_get_layer_sys(NULL), NULL); + lv_obj_set_hidden(obj, true); + obj = lv_obj_get_child(lv_disp_get_layer_sys(NULL), obj); + lv_obj_set_hidden(obj, true);*/ +} + +/** + * Create a demo application + */ +void haspSetup(JsonObject settings) +{ + char buffer[64]; + + haspSetConfig(settings); + +#ifdef LV_HASP_HOR_RES_MAX + lv_coord_t hres = LV_HASP_HOR_RES_MAX; +#else + lv_coord_t hres = lv_disp_get_hor_res(NULL); +#endif + +#ifdef LV_HASP_VER_RES_MAX + lv_coord_t vres = LV_HASP_VER_RES_MAX; +#else + lv_coord_t vres = lv_disp_get_ver_res(NULL); +#endif + + // static lv_font_t * + // my_font = (lv_font_t *)lv_mem_alloc(sizeof(lv_font_t)); + + my_font = (lv_font_t *)lv_mem_alloc(sizeof(lv_font_t)); + lv_zifont_init(); + //#ifdef ESP32 + + if(lv_zifont_font_init(my_font, haspZiFontPath.c_str(), 24) != 0) { + errorPrintln(String(F("HASP: %sFailed to set the custom font to ")) + haspZiFontPath); + my_font = NULL; // Use default font + } + + lv_theme_t * th; + switch(haspThemeId) { +#if LV_USE_THEME_ALIEN == 1 + case 1: + th = lv_theme_alien_init(haspThemeHue, my_font); + break; +#endif +#if LV_USE_THEME_NIGHT == 1 + case 2: + th = lv_theme_night_init(haspThemeHue, my_font); // heavy + break; +#endif +#if LV_USE_THEME_MONO == 1 + case 3: + th = lv_theme_mono_init(haspThemeHue, my_font); // lightweight + break; +#endif +#if LV_USE_THEME_MATERIAL == 1 + case 4: + th = lv_theme_material_init(haspThemeHue, my_font); + break; +#endif +#if LV_USE_THEME_ZEN == 1 + case 5: + th = lv_theme_zen_init(haspThemeHue, my_font); // lightweight + break; +#endif +#if LV_USE_THEME_NEMO == 1 + case 6: + th = lv_theme_nemo_init(haspThemeHue, my_font); // heavy + break; +#endif +#if LV_USE_THEME_TEMPL == 1 + case 7: + th = lv_theme_templ_init(haspThemeHue, my_font); // lightweight, not for production... + break; +#endif +#if LV_USE_THEME_HASP == 1 + case 8: + th = lv_theme_hasp_init(haspThemeHue, my_font); + break; +#endif + case 0: +#if LV_USE_THEME_DEFAULT == 1 + th = lv_theme_default_init(haspThemeHue, my_font); +#else + th = lv_theme_hasp_init(512, my_font); +#endif + break; + + default: + th = lv_theme_hasp_init(512, my_font); + debugPrintln(F("HASP: Unknown theme selected")); + } + + if(th) { + debugPrintln(F("HASP: Custom theme loaded")); + } else { + errorPrintln(F("HASP: %sNo theme could be loaded")); + } + lv_theme_set_current(th); + + /*Create a screen*/ + for(uint8_t i = 0; i < (sizeof pages / sizeof *pages); i++) { + pages[i] = lv_obj_create(NULL, NULL); + // lv_obj_set_size(pages[0], hres, vres); + } + + /* + lv_obj_t * obj; + + obj = lv_label_create(lv_layer_sys(), NULL); + lv_obj_set_size(obj, hres, 30); + lv_obj_set_pos(obj, lv_disp_get_hor_res(NULL) / 2 - 40, lv_disp_get_ver_res(NULL) / 2 + 40 + 20); + lv_label_set_text(obj, String(F("#ffffff " LV_SYMBOL_WIFI " Connecting... #")).c_str()); + lv_label_set_recolor(obj, true); + + obj = lv_btn_create(lv_layer_sys(), NULL); + lv_obj_set_size(obj, 80, 80); + lv_obj_set_pos(obj, lv_disp_get_hor_res(NULL) / 2 - 40, lv_disp_get_ver_res(NULL) / 2 - 40 - 20); + */ + haspDisconnect(); + haspLoadPage(); + haspSetPage(haspStartPage); + + // // lv_page_set_style(page, LV_PAGE_STYLE_SB, &style_sb); /*Set the scrollbar style*/ + + // // lv_obj_t *img1 = lv_img_create(pages[2], NULL); + // // lv_img_set_src(img1, &xmass); + + // // lv_obj_set_protect(page1, LV_PROTECT_POS); + // // lv_obj_set_protect(page2, LV_PROTECT_POS); + + // /*Create a page*/ + // /* lv_obj_t * page1 = lv_obj_create(screen, NULL); + // lv_obj_set_size(page1, hres, vres); + // lv_obj_align(page1, NULL, LV_ALIGN_CENTER, 0, 0); + // //lv_page_set_style(page, LV_PAGE_STYLE_SB, &style_sb); /*Set the scrollbar style*/ + + // /* default style*/ + // static lv_style_t style_btnm_rel; /* Une variable pour,!enregistrer le style normal */ + // lv_style_copy(&style_btnm_rel, &lv_style_plain); /* Initialise a partir d un,!style included */ + // style_btnm_rel.body.border.color = lv_color_hex3(0x269); + // style_btnm_rel.body.border.width = 1; + // style_btnm_rel.body.padding.inner = -5; + // style_btnm_rel.body.padding.left = 0; + // style_btnm_rel.body.padding.right = 0; + // style_btnm_rel.body.padding.bottom = 0; + // style_btnm_rel.body.padding.top = 0; + // style_btnm_rel.body.main_color = lv_color_hex3(0xADF); + // style_btnm_rel.body.grad_color = lv_color_hex3(0x46B); + // style_btnm_rel.body.shadow.width = 0; + // style_btnm_rel.body.radius = 0; + // // style_btnm_rel.body.shadow.type = LV_SHADOW_BOTTOM; + // // style_btnm_rel.body.radius = LV_RADIUS_CIRCLE; + // style_btnm_rel.text.color = lv_color_hex3(0xDEF); + + // static lv_style_t style_btnm_pr; /* Une variable pour,!enregistrer le style pressed */ + // lv_style_copy(&style_btnm_pr, &style_btnm_rel); /* Initialise a partir du,!style released */ + // style_btnm_pr.body.border.width = 1; + // style_btnm_pr.body.border.color = lv_color_hex3(0x46B); + // style_btnm_pr.body.main_color = lv_color_hex3(0x8BD); + // style_btnm_pr.body.grad_color = lv_color_hex3(0x24A); + // style_btnm_pr.body.shadow.width = 0; + // style_btnm_pr.body.radius = 0; + // style_btnm_pr.text.color = lv_color_hex3(0xBCD); + + // lv_obj_t * btnm1 = lv_btnm_create(pages[0], NULL); + // /* style should set before map */ + // lv_btnm_set_style(btnm1, LV_BTN_STYLE_REL, &style_btnm_rel); /* Dfinit le style,!released du bouton */ + // lv_btnm_set_style(btnm1, LV_BTN_STYLE_PR, &style_btnm_pr); /* Dfinit le style,!pressed du bouton */ + // lv_btnm_set_style(btnm1, LV_BTN_STYLE_TGL_REL, &style_btnm_rel); /* Dfinit le style,!released du bouton */ + // lv_btnm_set_style(btnm1, LV_BTN_STYLE_TGL_PR, &style_btnm_pr); /* Dfinit le style,!pressed du bouton */ + + // lv_btnm_set_map(btnm1, btnm_map1); + + // lv_btnm_set_style(btnm1, LV_BTNM_STYLE_BG, &lv_style_transp); + // lv_obj_set_size(btnm1, hres, vres); + // // lv_btnm_set_style(btnm1, LV_BTNM_STYLE, &style) + // // lv_btnm_set_btn_width(btnm1, 10, 2); /*Make "Action1" twice as wide as "Action2"*/ + // lv_obj_align(btnm1, NULL, LV_ALIGN_CENTER, 0, 0); + // lv_obj_set_event_cb(btnm1, btnmap_event_handler); + + // lv_obj_t * btnm2 = lv_btnm_create(pages[1], NULL); + // /* style should set before map */ + // lv_btnm_set_style(btnm2, LV_BTNM_STYLE_BG, &lv_style_transp); + // lv_btnm_set_map(btnm2, btnm_map2); + // lv_obj_set_size(btnm2, hres, vres); + // // lv_btnm_set_style(btnm1, LV_BTNM_STYLE, &style) + // // lv_btnm_set_btn_width(btnm1, 10, 2); /*Make "Action1" twice as wide as "Action2"*/ + // lv_obj_align(btnm2, NULL, LV_ALIGN_CENTER, 0, 0); + // lv_obj_set_event_cb(btnm2, btnmap_event_handler); +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +void haspLoop(void) +{ + /* idle detection and dim */ +} + +void hasp_background(uint16_t pageid, uint16_t imageid) +{ + lv_obj_t * page = get_page(pageid); + if(!page) return; + + return; + + page = lv_scr_act(); + lv_obj_t * thisobj = lv_obj_get_child_back(page, NULL); + + if(!thisobj) return; + + /* + switch (imageid) + { + case 0: + lv_img_set_src(thisobj, &frame00); + break; + case 1: + lv_img_set_src(thisobj, &frame02); + break; + case 2: + lv_img_set_src(thisobj, &frame04); + break; + case 3: + lv_img_set_src(thisobj, &frame06); + break; + case 4: + lv_img_set_src(thisobj, &frame08); + break; + case 5: + lv_img_set_src(thisobj, &frame10); + break; + case 6: + lv_img_set_src(thisobj, &frame12); + break; + case 7: + lv_img_set_src(thisobj, &frame14); + break; + } + //printf("Image set to %u\n", imageid); +*/ + lv_img_set_auto_size(thisobj, false); + lv_obj_set_width(thisobj, lv_disp_get_hor_res(NULL)); + lv_obj_set_height(thisobj, lv_disp_get_ver_res(NULL)); + // lv_obj_set_protect(wp, LV_PROTECT_POS); + // lv_obj_invalidate(thisobj); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Called when a a list button is clicked on the List tab + * @param btn pointer to a list button + * @param event type of event that occured + */ +static void btn_event_handler(lv_obj_t * obj, lv_event_t event) +{ + // int16_t id = get_obj_id(obj); + + uint8_t eventid = 0; + uint8_t pageid = 0; + lv_obj_user_data_t objid; + + if(!FindIdFromObj(obj, &pageid, &objid)) { + errorPrintln(F("HASP: %sEvent for unknown object")); + return; + } + + switch(event) { + case LV_EVENT_PRESSED: + debugPrintln(F("HASP: Pressed Down")); + eventid = 1; + break; + + case LV_EVENT_CLICKED: + debugPrintln(F("HASP: Released Up")); + // UP = the same object was release then was pressed and press was not lost! + eventid = 0; + break; + + case LV_EVENT_SHORT_CLICKED: + debugPrintln(F("HASP: Short Click")); + eventid = 2; + break; + + case LV_EVENT_PRESSING: + // printf("Pressing\n"); // Constant press events, do not send ! + return; + + case LV_EVENT_LONG_PRESSED: + debugPrintln(F("HASP: Long Press")); + eventid = 3; + break; + + case LV_EVENT_LONG_PRESSED_REPEAT: + debugPrintln(F("HASP: Long Press Repeat")); + eventid = 4; + break; + + case LV_EVENT_PRESS_LOST: + debugPrintln(F("HASP: Press Lost")); + eventid = 9; + break; + + case LV_EVENT_RELEASED: + // printf("p[%u].b[%u].val = %u", pageid, objid, 512); + // printf("Released\n\n"); // Not used, is also fired when dragged + return; + + /* + lv_obj_t * child = lv_obj_get_child(obj, NULL); + if(child) lv_obj_get_type(child, &buf); + printf("obj eight %s -> ", buf.type[0]); + */ + break; + + case LV_EVENT_VALUE_CHANGED: + debugPrintln(F("HASP: Value Changed Event occured")); + return; + + default: + debugPrintln(F("HASP: Unknown Event occured")); + return; + } + + // printf("p[%u].b[%u].val = %u ", pageid, objid, eventid); + mqttSendNewEvent(pageid, objid, eventid); +} + +static void btnmap_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) haspSendNewValue(obj, lv_btnm_get_pressed_btn(obj)); +} + +static void slider_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) haspSendNewValue(obj, lv_slider_get_value(obj)); +} + +static void cpicker_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) haspSendNewValue(obj, lv_cpicker_get_color(obj)); +} + +static void switch_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) haspSendNewValue(obj, lv_sw_get_state(obj)); +} + +static void ddlist_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) { + haspSendNewValue(obj, lv_ddlist_get_selected(obj)); + char buffer[100]; + lv_ddlist_get_selected_str(obj, buffer, sizeof(buffer)); + haspSendNewValue(obj, String(buffer)); + } +} + +static void roller_event_handler(lv_obj_t * obj, lv_event_t event) +{ + if(event == LV_EVENT_VALUE_CHANGED) { + haspSendNewValue(obj, lv_roller_get_selected(obj)); + char buffer[100]; + lv_roller_get_selected_str(obj, buffer, sizeof(buffer)); + haspSendNewValue(obj, String(buffer)); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void haspReset() +{ + mqttStop(); // Stop the MQTT Client first + configWriteConfig(); + debugPrintln(F("HASP: Reboot MCU")); + debugStop(); + delay(1000); + ESP.restart(); // nextionReset(); + delay(5000); +} + +void haspSetNodename(String name) +{} + +String haspGetNodename() +{ + return String(F("plate11")); +} + +float_t haspGetVersion() +{ + return 0.1; +} + +uint16_t haspGetPage() +{ + return current_page; +} + +void haspSetPage(uint16_t pageid) +{ + lv_obj_t * page = get_page(pageid); + if(!page) { + errorPrintln(F("HASP: %sPage ID not defined")); + } else if(page == lv_layer_sys() || page == lv_layer_top()) { + errorPrintln(F("HASP: %sCannot change to a layer")); + } else { + debugPrintln(String(F("HASP: Changing page to ")) + String(pageid)); + lv_scr_load(page); + current_page = pageid; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void haspNewObject(const JsonObject & config) +{ + /* Validate page and type */ + if(config[F("page")].isNull()) return; // comments + if(config[F("objid")].isNull()) return; // comments + + /* Page selection */ + uint8_t pageid = config[F("page")].as(); + lv_obj_t * page = get_page(pageid); + if(!page) { + errorPrintln(F("HASP: %sPage ID not defined")); + return; + } + + /* Input cache and validation */ + int16_t min = config[F("min")].as(); + int16_t max = config[F("max")].as(); + int16_t val = config[F("val")].as(); + if(min >= max) { + min = 0; + max = 100; + } + bool enabled = config[F("enable")] ? config[F("enable")].as() : true; + lv_coord_t width = config[F("w")].as(); + lv_coord_t height = config[F("h")].as(); + if(width == 0) width = 32; + if(height == 0) height = 32; + uint8_t objid = config[F("objid")].as(); + uint8_t id = config[F("id")].as(); + + /* Define Objects*/ + lv_obj_t * obj; + lv_obj_t * label; + switch(objid) { + case LV_HASP_BUTTON: { + obj = lv_btn_create(page, NULL); + bool toggle = config[F("toggle")].as(); + lv_btn_set_toggle(obj, toggle); + if(config[F("txt")]) { + label = lv_label_create(obj, NULL); + lv_label_set_text(label, config[F("txt")].as().c_str()); + lv_obj_set_opa_scale_enable(label, true); + lv_obj_set_opa_scale(label, LV_OPA_COVER); + } + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + case LV_HASP_CHECKBOX: { + obj = lv_cb_create(page, NULL); + if(config[F("txt")]) lv_cb_set_text(obj, config[F("txt")].as().c_str()); + // lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + case LV_HASP_LABEL: { + obj = lv_label_create(page, NULL); + if(config[F("txt")]) { + lv_label_set_text(obj, config[F("txt")].as().c_str()); + } + /* click area padding */ + uint8_t padh = config[F("padh")].as(); + uint8_t padv = config[F("padv")].as(); + /* text align */ + if(padh > 0 || padv > 0) { + lv_obj_set_ext_click_area(obj, padh, padh, padv, padv); + } + if(!config[F("align")].isNull()) { + lv_label_set_align(obj, LV_LABEL_ALIGN_CENTER); + } + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + /* ----- Color Objects ------ */ + case LV_HASP_CPICKER: { + obj = lv_cpicker_create(page, NULL); + // lv_cpicker_set_value(obj, (uint8_t)val); + bool rect = config[F("rect")].as(); + lv_cpicker_set_type(obj, rect ? LV_CPICKER_TYPE_RECT : LV_CPICKER_TYPE_DISC); + lv_obj_set_event_cb(obj, cpicker_event_handler); + break; + } +#if LV_USE_PRELOAD != 0 + case LV_HASP_PRELOADER: { + obj = lv_preload_create(page, NULL); + break; + } +#endif + /* ----- Range Objects ------ */ + case LV_HASP_SLIDER: { + obj = lv_slider_create(page, NULL); + lv_slider_set_range(obj, min, max); + lv_slider_set_value(obj, val, LV_ANIM_OFF); + lv_obj_set_event_cb(obj, slider_event_handler); + break; + } + case LV_HASP_GAUGE: { + obj = lv_gauge_create(page, NULL); + lv_gauge_set_range(obj, min, max); + lv_gauge_set_value(obj, val, LV_ANIM_OFF); + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + case LV_HASP_BAR: { + obj = lv_bar_create(page, NULL); + lv_bar_set_range(obj, min, max); + lv_bar_set_value(obj, val, LV_ANIM_OFF); + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + case LV_HASP_LMETER: { + obj = lv_lmeter_create(page, NULL); + lv_lmeter_set_range(obj, min, max); + lv_lmeter_set_value(obj, val); + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + + /* ----- On/Off Objects ------ */ + case LV_HASP_SWITCH: { + obj = lv_sw_create(page, NULL); + bool state = config[F("val")].as(); + if(state) lv_sw_on(obj, LV_ANIM_OFF); + lv_obj_set_event_cb(obj, switch_event_handler); + break; + } + case LV_HASP_LED: { + obj = lv_led_create(page, NULL); + bool state = config[F("val")].as(); + if(state) lv_led_on(obj); + lv_obj_set_event_cb(obj, btn_event_handler); + break; + } + /**/ + case LV_HASP_DDLIST: { + obj = lv_ddlist_create(page, NULL); + if(config[F("txt")]) lv_ddlist_set_options(obj, config[F("txt")].as().c_str()); + lv_ddlist_set_selected(obj, val); + lv_ddlist_set_fix_width(obj, width); + lv_ddlist_set_draw_arrow(obj, true); + lv_obj_set_top(obj, true); + // lv_obj_align(obj, NULL, LV_ALIGN_IN_TOP_MID, 0, 20); + lv_obj_set_event_cb(obj, ddlist_event_handler); + break; + } + case LV_HASP_ROLLER: { + obj = lv_roller_create(page, NULL); + bool infinite = config[F("infinite")].as(); + if(config[F("txt")]) lv_roller_set_options(obj, config[F("txt")].as().c_str(), infinite); + lv_roller_set_selected(obj, val, LV_ANIM_ON); + lv_roller_set_fix_width(obj, width); + lv_roller_set_visible_row_count(obj, config[F("rows")].as()); + // lv_obj_align(obj, NULL, LV_ALIGN_IN_TOP_MID, 0, 20); + lv_obj_set_event_cb(obj, roller_event_handler); + break; + } + + /* ----- Other Object ------ */ + default: + errorPrintln(F("HASP: %sUnsupported Object ID")); + return; + } + + if(!obj) { + errorPrintln(F("HASP: %sObject is NULL")); + return; + } + + if(!config[F("opacity")].isNull()) { + uint8_t opacity = config[F("opacity")].as(); + lv_obj_set_opa_scale_enable(obj, opacity < 100); + lv_obj_set_opa_scale(obj, opacity < 100 ? opacity : 100); + } + + bool hidden = config[F("hidden")].as(); + lv_obj_set_hidden(obj, hidden); + lv_obj_set_click(obj, enabled); + + lv_obj_set_pos(obj, config[F("x")].as(), config[F("y")].as()); + lv_obj_set_width(obj, width); + if(objid != LV_HASP_DDLIST && objid != LV_HASP_ROLLER) + lv_obj_set_height(obj, height); // ddlist and roller have auto height + + lv_obj_set_user_data(obj, id); + + /** testing start **/ + lv_obj_user_data_t temp; + if(!FindIdFromObj(obj, &pageid, &temp)) { + errorPrintln(F("HASP: %sLost track of the created object, not found!")); + return; + } + /** testing end **/ + + char msg[64]; + sprintf_P(msg, PSTR("HASP: Created object p[%u].b[%u]"), pageid, temp); + debugPrintln(msg); + + /* Double-check */ + lv_obj_t * test = FindObjFromId(pageid, (uint8_t)temp); + if(test == obj) { + debugPrintln(F("Objects match!")); + } else { + errorPrintln(F("HASP: %sObjects DO NOT match!")); + } +} + +void haspLoadPage() +{ + if(!SPIFFS.begin()) { + errorPrintln(F("HASP: %sFS not mounted. Failed to load /pages.jsonl")); + return; + } + + debugPrintln(F("HASP: Loading /pages.jsonl")); + File file = SPIFFS.open(haspPagesPath, "r"); + // ReadBufferingStream bufferingStream(file, 256); + DynamicJsonDocument config(256); + + while(deserializeJson(config, file) == DeserializationError::Ok) { + serializeJson(config, Serial); + Serial.println(); + haspNewObject(config.as()); + } + + debugPrintln(F("HASP: /pages.jsonl loaded")); + file.close(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool haspGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[FPSTR(F_CONFIG_STARTPAGE)] == haspStartPage && + settings[FPSTR(F_CONFIG_THEME)] == haspThemeId && settings[FPSTR(F_CONFIG_HUE)] == haspThemeHue && + settings[FPSTR(F_CONFIG_ZIFONT)] == haspZiFontPath && settings[FPSTR(F_CONFIG_PAGES)] == haspPagesPath) + return false; + + settings[FPSTR(F_CONFIG_STARTPAGE)] = haspStartPage; + settings[FPSTR(F_CONFIG_THEME)] = haspThemeId; + settings[FPSTR(F_CONFIG_HUE)] = haspThemeHue; + settings[FPSTR(F_CONFIG_ZIFONT)] = haspZiFontPath; + settings[FPSTR(F_CONFIG_PAGES)] = haspPagesPath; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool haspSetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[FPSTR(F_CONFIG_STARTPAGE)] == haspStartPage && + settings[FPSTR(F_CONFIG_THEME)] == haspThemeId && settings[FPSTR(F_CONFIG_HUE)] == haspThemeHue && + settings[FPSTR(F_CONFIG_ZIFONT)] == haspZiFontPath && settings[FPSTR(F_CONFIG_PAGES)] == haspPagesPath) + return false; + + bool changed = false; + + if(!settings[FPSTR(F_CONFIG_PAGES)].isNull()) { + if(haspPagesPath != settings[FPSTR(F_CONFIG_PAGES)].as().c_str()) { + debugPrintln(F("haspPagesPath changed")); + } + changed |= haspPagesPath != settings[FPSTR(F_CONFIG_PAGES)].as().c_str(); + + haspPagesPath = settings[FPSTR(F_CONFIG_PAGES)].as().c_str(); + } + + if(!settings[FPSTR(F_CONFIG_ZIFONT)].isNull()) { + if(haspZiFontPath != settings[FPSTR(F_CONFIG_ZIFONT)].as().c_str()) { + debugPrintln(F("haspZiFontPath changed")); + } + changed |= haspZiFontPath != settings[FPSTR(F_CONFIG_ZIFONT)].as().c_str(); + + haspZiFontPath = settings[FPSTR(F_CONFIG_ZIFONT)].as().c_str(); + } + + if(!settings[FPSTR(F_CONFIG_STARTPAGE)].isNull()) { + if(haspStartPage != settings[FPSTR(F_CONFIG_STARTPAGE)].as()) { + debugPrintln(F("haspStartPage changed")); + } + changed |= haspStartPage != settings[FPSTR(F_CONFIG_STARTPAGE)].as(); + + haspStartPage = settings[FPSTR(F_CONFIG_STARTPAGE)].as(); + } + + if(!settings[FPSTR(F_CONFIG_THEME)].isNull()) { + if(haspThemeId != settings[FPSTR(F_CONFIG_THEME)].as()) { + debugPrintln(F("haspThemeId changed")); + } + changed |= haspThemeId != settings[FPSTR(F_CONFIG_THEME)].as(); + + haspThemeId = settings[FPSTR(F_CONFIG_THEME)].as(); + } + + if(!settings[FPSTR(F_CONFIG_HUE)].isNull()) { + if(haspThemeHue != settings[FPSTR(F_CONFIG_HUE)].as()) { + debugPrintln(F("haspStartPage changed")); + } + changed |= haspThemeHue != settings[FPSTR(F_CONFIG_HUE)].as(); + + haspThemeHue = settings[FPSTR(F_CONFIG_HUE)].as(); + } + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return changed; +} \ No newline at end of file diff --git a/src/hasp_config.cpp b/src/hasp_config.cpp new file mode 100644 index 00000000..ec738adb --- /dev/null +++ b/src/hasp_config.cpp @@ -0,0 +1,145 @@ +#include "Arduino.h" +#include "ArduinoJson.h" + +#ifdef ESP32 +#include "SPIFFS.h" +#endif +#include // Include the SPIFFS library + +#include "hasp_config.h" +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_http.h" +#include "hasp_mqtt.h" +#include "hasp_wifi.h" +#include "hasp_mdns.h" +#include "hasp_gui.h" +#include "hasp_tft.h" +#include "hasp_ota.h" +#include "hasp.h" + +#define HASP_CONFIG_FILE F("/config.json") + +void spiffsList() +{ +#if defined(ARDUINO_ARCH_ESP32) + debugPrintln(F("FILE: Listing files on the internal flash:")); + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + while(file) { + char msg[64]; + sprintf_P(msg, PSTR("FILE: * %s (%u bytes)"), file.name(), (uint32_t)file.size()); + debugPrintln(msg); + file = root.openNextFile(); + } +#endif +#if defined(ARDUINO_ARCH_ESP8266) + debugPrintln(F("FILE: Listing files on the internal flash:")); + Dir dir = SPIFFS.openDir("/"); + while(dir.next()) { + char msg[64]; + sprintf_P(msg, PSTR("FILE: * %s (%u bytes)"), dir.fileName().c_str(), (uint32_t)dir.fileSize()); + debugPrintln(msg); + } +#endif +} + +bool configChanged() +{ + return false; +} + +void configLoop() +{ + if(configChanged()) { + // configSetConfig(); + } +} + +void configGetConfig(JsonDocument & settings, bool setupdebug = false) +{ + File file = SPIFFS.open(HASP_CONFIG_FILE, "r"); + + if(file) { + size_t size = file.size(); + if(size > 1024) { + errorPrintln(F("CONF: %sConfig file size is too large")); + return; + } + + DeserializationError error = deserializeJson(settings, file); + if(!error) { + file.close(); + + if(setupdebug) { + debugSetup(); // Debug started, now we can use it; HASP header sent + debugPrintln(F("FILE: [SUCCESS] SPI flash FS mounted")); + spiffsList(); + } + debugPrintln(String(F("CONF: Loading ")) + String(HASP_CONFIG_FILE)); + + // show settings in log + String output; + serializeJson(settings, output); + debugPrintln(String(F("CONF: ")) + output); + + debugPrintln(String(F("CONF: [SUCCESS] Loaded ")) + String(HASP_CONFIG_FILE)); + return; + } + } + + if(setupdebug) { + // setup debugging defaults + debugSetup(); // Debug started, now we can use it; HASP header sent + debugPrintln(F("FILE: [SUCCESS] SPI flash FS mounted")); + spiffsList(); + } + debugPrintln(String(F("CONF: Loading ")) + String(HASP_CONFIG_FILE)); + errorPrintln(String(F("CONF: %sFailed to load ")) + String(HASP_CONFIG_FILE)); +} + +void configWriteConfig() +{ + /* Read Config File */ + DynamicJsonDocument settings(1024); + debugPrintln(String(F("CONF: Config LOADING first")) + String(HASP_CONFIG_FILE)); + configGetConfig(settings, false); + debugPrintln(String(F("CONF: Config LOADED first")) + String(HASP_CONFIG_FILE)); + + bool changed = true; + // changed |= debugGetConfig(settings[F("debug")].to()); + // changed |= guiGetConfig(settings[F("gui")].to()); + changed |= haspGetConfig(settings[F("hasp")].to()); + // changed |= httpGetConfig(settings[F("http")].to()); + // changed |= mdnsGetConfig(settings[F("mdns")].to()); + // changed |= mqttGetConfig(settings[F("mqtt")].to()); + // changed |= otaGetConfig(settings[F("ota")].to()); + // changed |= tftGetConfig(settings[F("tft")].to()); + // changed |= wifiGetConfig(settings[F("wifi")].to()); + + if(changed) { + File file = SPIFFS.open(HASP_CONFIG_FILE, "w"); + if(file) { + debugPrintln(F("CONF: Writing /config.json")); + size_t size = serializeJson(settings, file); + file.close(); + if(size > 0) { + debugPrintln(F("CONF: [SUCCESS] /config.json saved")); + return; + } + } + + errorPrintln(F("CONF: %sFailed to write /config.json")); + } else { + debugPrintln(F("CONF: Configuration was not changed")); + } +} + +void configSetup(JsonDocument & settings) +{ + if(!SPIFFS.begin()) { + errorPrintln(F("FILE: %sSPI flash init failed. Unable to mount FS.")); + } else { + configGetConfig(settings, true); + } +} \ No newline at end of file diff --git a/src/hasp_debug.cpp b/src/hasp_debug.cpp new file mode 100644 index 00000000..57af2f67 --- /dev/null +++ b/src/hasp_debug.cpp @@ -0,0 +1,74 @@ +#include +#include "ArduinoJson.h" + +#ifdef ESP8266 +#include +#include +#else +#include +#endif +#include +#include + +#include "hasp_debug.h" +#include "hasp_config.h" + +#include "user_config_override.h" + +#ifndef SYSLOG_SERVER +#define SYSLOG_SERVER "" +#endif +#ifndef SYSLOG_PORT +#define SYSLOG_PORT 514 +#endif +#ifndef APP_NAME +#define APP_NAME "HASP" +#endif + +std::string debugAppName = APP_NAME; +std::string debugSyslogHost = SYSLOG_SERVER; +uint16_t debugSyslogPort = SYSLOG_PORT; + +// A UDP instance to let us send and receive packets over UDP +WiFiUDP syslogClient; + +// Create a new syslog instance with LOG_KERN facility +// Syslog syslog(syslogClient, SYSLOG_SERVER, SYSLOG_PORT, MQTT_CLIENT, APP_NAME, LOG_KERN); +// Create a new empty syslog instance +Syslog syslog(syslogClient, debugSyslogHost.c_str(), debugSyslogPort, debugAppName.c_str(), debugAppName.c_str(), + LOG_LOCAL0); + +void debugSetup() +{ + Serial.begin(115200); /* prepare for possible serial debug */ + Serial.flush(); + Serial.println(); + Serial.println(); + Serial.println(F("\n _____ _____ _____ _____\n | | | _ | __| _ |\n" + " | | |__ | __|\n |__|__|__|__|_____|__|\n" + " Home Automation Switch Plate\n Open Hardware edition\n\n")); + Serial.flush(); + + // prepare syslog configuration here (can be anywhere before first call of + // log/logf method) + + syslog.server(debugSyslogHost.c_str(), debugSyslogPort); + syslog.deviceHostname(debugAppName.c_str()); + syslog.appName(debugAppName.c_str()); + syslog.defaultPriority(LOG_LOCAL0); +} + +void debugLoop() +{} + +void serialPrintln(String debugText) +{ + String debugTimeText = + "[+" + String(float(millis()) / 1000, 3) + "s] " + String(ESP.getFreeHeap()) + " " + debugText; + Serial.println(debugTimeText); +} + +void debugStop() +{ + Serial.flush(); +} \ No newline at end of file diff --git a/src/hasp_eeprom.cpp b/src/hasp_eeprom.cpp new file mode 100644 index 00000000..2b130cbe --- /dev/null +++ b/src/hasp_eeprom.cpp @@ -0,0 +1,50 @@ +#include +#include + +#include "hasp_debug.h" + +void eepromWrite(char addr, std::string & data); +std::string eepromRead(char addr); + +void eepromSetup() +{ + EEPROM.begin(1024); + // debugPrintln("EEPROM: Started Eeprom"); +} + +void eepromLoop() +{} + +void eepromUpdate(uint16_t addr, char ch) +{ + if(EEPROM.read(addr) != ch) { + EEPROM.write(addr, ch); + } +} + +void eepromWrite(uint16_t addr, std::string & data) +{ + int count = data.length(); + for(int i = 0; i < count; i++) { + eepromUpdate(addr + i, data[i]); + } + eepromUpdate(addr + count, '\0'); + EEPROM.commit(); +} + +std::string eepromRead(uint16_t addr) +{ + int i; + char data[1024]; // Max 1024 Bytes + int len = 0; + unsigned char k; + k = EEPROM.read(addr); + while(k != '\0' && len < 1023) // Read until null character + { + k = EEPROM.read(addr + len); + if((uint8_t(k) < 32) || (uint8_t(k) > 127)) break; // check for printable ascii, includes '\0' + data[len] = k; + len++; + } + return std::string(data); +} \ No newline at end of file diff --git a/src/hasp_gui.cpp b/src/hasp_gui.cpp new file mode 100644 index 00000000..75ed07df --- /dev/null +++ b/src/hasp_gui.cpp @@ -0,0 +1,233 @@ +#include + +#include "lvgl.h" +#include "lv_conf.h" + +#include "TFT_eSPI.h" + +#ifdef ESP32 +#include "png_decoder.h" +#endif +#include "lv_zifont.h" + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_config.h" +#include "hasp_gui.h" + +#define LVGL_TICK_PERIOD 30 // 30 + +uint16_t guiSleepTime = 150; // 0.1 second resolution +bool guiSleeping = false; +uint8_t guiTickPeriod = 50; +Ticker tick; /* timer for interrupt handler */ +TFT_eSPI tft = TFT_eSPI(); /* TFT instance */ + +bool IRAM_ATTR guiCheckSleep() +{ + bool shouldSleep = lv_disp_get_inactive_time(NULL) > guiSleepTime * 100; + if(shouldSleep && !guiSleeping) { + debugPrintln(F("GUI: Going to sleep now...")); + guiSleeping = true; + } + return shouldSleep; +} + +#if LV_USE_LOG != 0 +/* Serial debugging */ +void debugLvgl(lv_log_level_t level, const char * file, uint32_t line, const char * dsc) +{ + char msg[128]; + sprintf(msg, PSTR("LVGL: %s@%d->%s"), file, line, dsc); + debugPrintln(msg); +} +#endif + +/* Display flushing */ +void tft_espi_flush(lv_disp_drv_t * disp, const lv_area_t * area, lv_color_t * color_p) +{ + uint16_t c; + + tft.startWrite(); /* Start new TFT transaction */ + tft.setAddrWindow(area->x1, area->y1, (area->x2 - area->x1 + 1), + (area->y2 - area->y1 + 1)); /* set the working window */ + for(int y = area->y1; y <= area->y2; y++) { + for(int x = area->x1; x <= area->x2; x++) { + c = color_p->full; + tft.writeColor(c, 1); + color_p++; + } + } + tft.endWrite(); /* terminate TFT transaction */ + lv_disp_flush_ready(disp); /* tell lvgl that flushing is done */ +} + +/* Interrupt driven periodic handler */ +static void IRAM_ATTR lv_tick_handler(void) +{ + lv_tick_inc(guiTickPeriod); +} + +/* Reading input device (simulated encoder here) */ +bool read_encoder(lv_indev_drv_t * indev, lv_indev_data_t * data) +{ + static int32_t last_diff = 0; + int32_t diff = 0; /* Dummy - no movement */ + int btn_state = LV_INDEV_STATE_REL; /* Dummy - no press */ + + data->enc_diff = diff - last_diff; + data->state = btn_state; + last_diff = diff; + return false; +} + +bool my_touchpad_read(lv_indev_drv_t * indev_driver, lv_indev_data_t * data) +{ + uint16_t touchX, touchY; + + bool touched = tft.getTouch(&touchX, &touchY, 600); + if(!touched) return false; + + bool shouldSleep = guiCheckSleep(); + if(!shouldSleep && guiSleeping) { + debugPrintln(F("GUI: Waking up!")); + guiSleeping = false; + } + + // Ignore first press? + + if(touchX > tft.width() || touchY > tft.height()) { + Serial.print(F("Y or y outside of expected parameters.. x: ")); + Serial.print(touchX); + Serial.print(F(" / y: ")); + Serial.println(touchY); + } else { + /*Save the state and save the pressed coordinate*/ + data->state = touched ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL; + data->point.x = touchX; + data->point.y = touchY; + /* + Serial.print("Data x"); + Serial.println(touchX); + + Serial.print("Data y"); + Serial.println(touchY);*/ + } + + return false; /*Return `false` because we are not buffering and no more data to read*/ +} + +void guiSetup(TFT_eSPI & screen, JsonObject settings) +{ + size_t buffer_size; + tft = screen; + lv_init(); + +#if ESP32 + /* allocate on iram (or psram ?) */ + buffer_size = 1024 * 8; + static lv_color_t * guiVdbBuffer = (lv_color_t *)malloc(sizeof(lv_color_t) * buffer_size); + static lv_disp_buf_t disp_buf; + lv_disp_buf_init(&disp_buf, guiVdbBuffer, NULL, buffer_size); +#else + /* allocate on heap */ + static lv_color_t guiVdbBuffer[1024 * 4]; + buffer_size = sizeof(guiVdbBuffer) / sizeof(guiVdbBuffer[0]); + static lv_disp_buf_t disp_buf; + lv_disp_buf_init(&disp_buf, guiVdbBuffer, NULL, buffer_size); +#endif + debugPrintln(String(F("LVGL: VDB size : ")) + String(buffer_size)); + +#if LV_USE_LOG != 0 + debugPrintln(F("LVGL: Registering lvgl logging handler")); + lv_log_register_print_cb(debugLvgl); /* register print function for debugging */ +#endif + + /* Initialize PNG decoder */ + // png_decoder_init(); + + /* Initialize the display driver */ + lv_disp_drv_t disp_drv; + lv_disp_drv_init(&disp_drv); + disp_drv.flush_cb = tft_espi_flush; + disp_drv.buffer = &disp_buf; +#if(TFT_ROTATION == 0 || TFT_ROTATION == 2 || TFT_ROTATION == 4 || TFT_ROTATION == 6) + /* 1/3=Landscape or 0/2=Portrait orientation */ + // Normal width & height + disp_drv.hor_res = TFT_WIDTH; // From User_Setup.h + disp_drv.ver_res = TFT_HEIGHT; // From User_Setup.h +#else + // Swapped width & height + disp_drv.hor_res = TFT_HEIGHT; // From User_Setup.h + disp_drv.ver_res = TFT_WIDTH; // From User_Setup.h +#endif + lv_disp_drv_register(&disp_drv); + + /*Initialize the touch pad*/ + lv_indev_drv_t indev_drv; + lv_indev_drv_init(&indev_drv); + // indev_drv.type = LV_INDEV_TYPE_ENCODER; + // indev_drv.read_cb = read_encoder; + indev_drv.type = LV_INDEV_TYPE_POINTER; + indev_drv.read_cb = my_touchpad_read; + lv_indev_t * mouse_indev = lv_indev_drv_register(&indev_drv); + + lv_obj_t * label = lv_label_create(lv_layer_sys(), NULL); + lv_label_set_text(label, "<"); + lv_indev_set_cursor(mouse_indev, label); // connect the object to the driver + + /* + lv_obj_t * cursor = lv_obj_create(lv_layer_sys(), NULL); // show on every page + lv_obj_set_size(cursor, 9, 9); + static lv_style_t style_cursor; + lv_style_copy(&style_cursor, &lv_style_pretty); + style_cursor.body.radius = LV_RADIUS_CIRCLE; + style_cursor.body.main_color = LV_COLOR_RED; + style_cursor.body.opa = LV_OPA_COVER; + lv_obj_set_style(cursor, &style_cursor); + // lv_obj_set_click(cursor, false); + lv_indev_set_cursor(mouse_indev, cursor); // connect the object to the driver + */ + /* Initialize mouse pointer */ + /*// if(true) { + debugPrintln(PSTR("LVGL: Initialize Cursor")); + lv_obj_t * cursor; + lv_obj_t * mouse_layer = lv_disp_get_layer_sys(NULL); // default display + // cursor = lv_obj_create(lv_scr_act(), NULL); + cursor = lv_obj_create(mouse_layer, NULL); // show on every page + lv_obj_set_size(cursor, 9, 9); + static lv_style_t style_round; + lv_style_copy(&style_round, &lv_style_plain); + style_round.body.radius = LV_RADIUS_CIRCLE; + style_round.body.main_color = LV_COLOR_RED; + style_round.body.opa = LV_OPA_COVER; + lv_obj_set_style(cursor, &style_round); + lv_obj_set_click(cursor, false); // don't click on the cursor + lv_indev_set_cursor(mouse_indev, cursor); + // }*/ + + /*Initialize the graphics library's tick*/ + tick.attach_ms(guiTickPeriod, lv_tick_handler); + + // guiLoop(); +} + +void IRAM_ATTR guiLoop() +{ + lv_task_handler(); /* let the GUI do its work */ + guiCheckSleep(); +} +void guiStop() +{} + +bool guiGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_GUI_TICKPERIOD] == guiTickPeriod) return false; + + settings[F_GUI_TICKPERIOD] = guiTickPeriod; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/hasp_http.cpp b/src/hasp_http.cpp new file mode 100644 index 00000000..41ac2b6a --- /dev/null +++ b/src/hasp_http.cpp @@ -0,0 +1,1066 @@ +//#include "webServer.h" +#include +#include "ArduinoJson.h" + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_http.h" +#include "hasp_mqtt.h" +#include "hasp_wifi.h" +#include "hasp_config.h" +#include "hasp.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "SPIFFS.h" +#endif +#include +#include + +#define F_HTTP_ENABLE F("enable") +#define F_HTTP_PORT F("port") +#define F_HTTP_USER F("user") +#define F_HTTP_PASS F("pass") + +bool httpEnable = true; +bool webServerStarted = false; +uint16_t httpPort = 80; +FS * filesystem = &SPIFFS; +File fsUploadFile; +String httpUser = "admin"; +String httpPassword = ""; + +#if defined(ARDUINO_ARCH_ESP8266) +#include +ESP8266WebServer webServer(80); +#endif + +#if defined(ARDUINO_ARCH_ESP32) +#include // needed to get the ResetInfo +#include +WebServer webServer(80); + +// Compatibility function for ESP8266 getRestInfo +String esp32ResetReason(uint8_t cpuid) +{ + if(cpuid > 1) { + return F("Invalid CPU id"); + } + RESET_REASON reason = rtc_get_reset_reason(cpuid); + + switch(reason) { + case 1: + return F("POWERON_RESET"); + break; /**<1, Vbat power on reset*/ + case 3: + return F("SW_RESET"); + break; /**<3, Software reset digital core*/ + case 4: + return F("OWDT_RESET"); + break; /**<4, Legacy watch dog reset digital core*/ + case 5: + return F("DEEPSLEEP_RESET"); + break; /**<5, Deep Sleep reset digital core*/ + case 6: + return F("SDIO_RESET"); + break; /**<6, Reset by SLC module, reset digital core*/ + case 7: + return F("TG0WDT_SYS_RESET"); + break; /**<7, Timer Group0 Watch dog reset digital core*/ + case 8: + return F("TG1WDT_SYS_RESET"); + break; /**<8, Timer Group1 Watch dog reset digital core*/ + case 9: + return F("RTCWDT_SYS_RESET"); + break; /**<9, RTC Watch dog Reset digital core*/ + case 10: + return F("INTRUSION_RESET"); + break; /**<10, Instrusion tested to reset CPU*/ + case 11: + return F("TGWDT_CPU_RESET"); + break; /**<11, Time Group reset CPU*/ + case 12: + return F("SW_CPU_RESET"); + break; /**<12, Software reset CPU*/ + case 13: + return F("RTCWDT_CPU_RESET"); + break; /**<13, RTC Watch dog Reset CPU*/ + case 14: + return F("EXT_CPU_RESET"); + break; /**<14, for APP CPU, reseted by PRO CPU*/ + case 15: + return F("RTCWDT_BROWN_OUT_RESET"); + break; /**<15, Reset when the vdd voltage is not stable*/ + case 16: + return F("RTCWDT_RTC_RESET"); + break; /**<16, RTC Watch dog reset digital core and rtc module*/ + default: + return F("NO_MEAN"); + } +} + +// these need to be removed +const uint8_t D0 = 0; +const uint8_t D1 = 1; +const uint8_t D2 = 2; +#endif // ESP32 + +static const char HTTP_DOCTYPE[] PROGMEM = + ""; +static const char HTTP_META_GO_BACK[] PROGMEM = ""; +static const char HTTP_HEADER[] PROGMEM = "%s"; +static const char HTTP_STYLE[] PROGMEM = + ""; +static const char HTTP_SCRIPT[] PROGMEM = ""; +static const char HTTP_HEADER_END[] PROGMEM = + "
"; +static const char HTTP_END[] PROGMEM = ""; +// Additional CSS style to match Hass theme +static const char HASP_STYLE[] PROGMEM = + ""; + +// these need to be removed +uint8_t motionPin = 0; // GPIO input pin for motion sensor if connected and enabled +bool debugSerialEnabled = true; // Enable USB serial debug output +bool debugTelnetEnabled = false; // Enable telnet debug output + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// These defaults may be overwritten with values saved by the web interface +char motionPinConfig[3] = "0"; +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// URL for auto-update "version.json" +const char UPDATE_URL[] = "http://haswitchplate.com/update/version.json"; +// Default link to compiled Arduino firmware image +String espFirmwareUrl = "http://haswitchplate.com/update/HASwitchPlate.ino.d1_mini.bin"; +// Default link to compiled Nextion firmware images +String lcdFirmwareUrl = "http://haswitchplate.com/update/HASwitchPlate.tft"; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +String formatBytes(size_t bytes); +void webHandleHaspConfig(); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool httpIsAuthenticated(const String & page) +{ + if(httpPassword[0] != '\0') { // Request HTTP auth if httpPassword is set + if(!webServer.authenticate(httpUser.c_str(), httpPassword.c_str())) { + webServer.requestAuthentication(); + return false; + } + } + char buffer[128]; + sprintf(buffer, PSTR("HTTP: Sending %s page to client connected from: %s"), page.c_str(), + webServer.client().remoteIP().toString().c_str()); + debugPrintln(buffer); + return true; +} + +String getOption(uint8_t value, String label, bool selected) +{ + char buffer[128]; + sprintf_P(buffer, PSTR(""), value, (selected ? PSTR("selected") : ""), + label.c_str()); + return buffer; +} +String getOption(String value, String label, bool selected) +{ + char buffer[128]; + sprintf_P(buffer, PSTR(""), value.c_str(), (selected ? PSTR("selected") : ""), + label.c_str()); + return buffer; +} + +void webSendPage(String & nodename, uint32_t httpdatalength, bool gohome = false) +{ + char buffer[64]; + + /* Calculate Content Length upfront */ + uint16_t contentLength = 0; + contentLength += sizeof(HTTP_DOCTYPE) - 1; + contentLength += sizeof(HTTP_HEADER) - 1 - 2 + nodename.length(); + contentLength += sizeof(HTTP_SCRIPT) - 1; + contentLength += sizeof(HTTP_STYLE) - 1; + contentLength += sizeof(HASP_STYLE) - 1; + if(gohome) contentLength += sizeof(HTTP_META_GO_BACK) - 1; + contentLength += sizeof(HTTP_HEADER_END) - 1; + contentLength += sizeof(HTTP_END) - 1; + + webServer.setContentLength(contentLength + httpdatalength); + + webServer.send_P(200, PSTR("text/html"), HTTP_DOCTYPE); // 122 + sprintf_P(buffer, HTTP_HEADER, nodename.c_str()); + webServer.sendContent(buffer); // 17-2+len + webServer.sendContent_P(HTTP_SCRIPT); // 131 + webServer.sendContent_P(HTTP_STYLE); // 487 + webServer.sendContent_P(HASP_STYLE); // 145 + if(gohome) webServer.sendContent_P(HTTP_META_GO_BACK); // 47 + webServer.sendContent_P(HTTP_HEADER_END); // 80 +} + +void webHandleRoot() +{ + if(!httpIsAuthenticated(F("root"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += String(F("

")); + httpMessage += String(nodename); + httpMessage += String(F("

")); + + httpMessage += F("

"); + httpMessage += F("

"); + + httpMessage += + F("

"); + + if(SPIFFS.exists(F("/edit.htm.gz"))) { + httpMessage += F( + "

"); + } + + httpMessage += + F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleAbout() +{ // http://plate01/about + if(!httpIsAuthenticated(F("/about"))) return; + + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1250); + + httpMessage += F("

HASP OpenHardware edition

Copyright© 2020 Francis Van Roie " + "
MIT License

"); + httpMessage += F("

Based on the previous work of the following open source developers.


"); + httpMessage += + F("

HASwitchPlate

Copyright© 2019 Allen Derusha allen@derusha.org
MIT License

"); + httpMessage += + F("

LittlevGL

Copyright© 2016 Gábor Kiss-Vámosi
Copyright© 2019 " + "LittlevGL
MIT License

"); + httpMessage += F("

Lvgl ziFont Font Engine

Copyright© 2020 Francis Van Roie
MIT License

"); + httpMessage += F("

TFT_eSPI Library

Copyright© 2017 Bodmer (https://github.com/Bodmer) All " + "rights reserved.
FreeBSD License
"); + httpMessage += + F("includes parts from the Adafruit_GFX library - Copyright© 2012 Adafruit Industries. All rights " + "reserved. BSD License

"); + httpMessage += F("

ArduinoJson

Copyright© 2014-2019 Benoit BLANCHON
MIT License

"); + httpMessage += F("

PubSubClient

Copyright© 2008-2015 Nicholas O'Leary
MIT License

"); + httpMessage += F("

Syslog

Copyright© 2016 Martin Sloup
MIT License

"); + + httpMessage += F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleInfo() +{ // http://plate01/ + if(!httpIsAuthenticated(F("/info"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += F("
MQTT Status: "); + if(mqttIsConnected()) { // Check MQTT connection + httpMessage += String(F("Connected")); + } else { + httpMessage += String(F("Disconnected, return code: ")); + // +String(mqttClient.returnCode()); + } + httpMessage += String(F("
MQTT ClientID: ")); + // +String(mqttClientId); + httpMessage += F("
HASP Version: "); + httpMessage += String(haspGetVersion()); + httpMessage += F("
Uptime: "); + httpMessage += String(long(millis() / 1000)); + + // httpMessage += String(F("
LCD Model: ")) + String(LV_HASP_HOR_RES_MAX) + " x " + + // String(LV_HASP_VER_RES_MAX); httpMessage += String(F("
LCD Version: ")) + String(lcdVersion); + httpMessage += F("

LCD Active Page: "); + httpMessage += String(haspGetPage()); + httpMessage += F("
CPU Frequency: "); + httpMessage += String(ESP.getCpuFreqMHz()); + + httpMessage += F("MHz

SSID: "); + httpMessage += String(WiFi.SSID()); + httpMessage += F("
Signal Strength: "); + httpMessage += String(WiFi.RSSI()); + httpMessage += F("
IP Address: "); + httpMessage += String(WiFi.localIP().toString()); + httpMessage += F("
Gateway: "); + httpMessage += String(WiFi.gatewayIP().toString()); + httpMessage += F("
DNS Server: "); + httpMessage += String(WiFi.dnsIP().toString()); + httpMessage += F("
MAC Aress: "); + httpMessage += String(WiFi.macAddress()); + + httpMessage += F("

ESP Chip Id: "); +#if defined(ARDUINO_ARCH_ESP32) + httpMessage += String(ESP.getChipRevision()); +#else + httpMessage += String(ESP.getChipId()); +#endif + httpMessage += F("
Flash Chip Size: "); + httpMessage += formatBytes(ESP.getFlashChipSize()); + httpMessage += F("
Program Size: "); + httpMessage += formatBytes(ESP.getSketchSize()); + httpMessage += F(" bytes
Free Program Space: "); + httpMessage += formatBytes(ESP.getFreeSketchSpace()); + httpMessage += F(" bytes
Free Memory: "); + httpMessage += formatBytes(ESP.getFreeHeap()); + +#if defined(ARDUINO_ARCH_ESP32) + // httpMessage += F("
Heap Max Alloc: "); + // httpMessage += String(ESP.getMaxAllocHeap()); + httpMessage += F("
Memory Fragmentation: "); + httpMessage += String((int16_t)(100.00f - (float)ESP.getMaxAllocHeap() / (float)ESP.getFreeHeap() * 100.00f)); + httpMessage += F("
ESP SDK version: "); + httpMessage += String(ESP.getSdkVersion()); + httpMessage += F("
Last Reset: CPU0: "); + httpMessage += String(esp32ResetReason(0)); + httpMessage += F(" / CPU1: "); + httpMessage += String(esp32ResetReason(1)); +#else + httpMessage += F("
Memory Fragmentation: "); + httpMessage += String(ESP.getHeapFragmentation()); + httpMessage += F("
ESP Core version: "); + httpMessage += String(ESP.getCoreVersion()); + httpMessage += F("
Last Reset: "); + httpMessage += String(ESP.getResetInfo()); +#endif + + httpMessage += F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +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"; + } +} + +String getContentType(String filename) +{ + if(webServer.hasArg(F("download"))) { + return F("application/octet-stream"); + } else if(filename.endsWith(F(".htm"))) { + return F("text/html"); + } else if(filename.endsWith(F(".html"))) { + return F("text/html"); + } else if(filename.endsWith(F(".css"))) { + return F("text/css"); + } else if(filename.endsWith(F(".js"))) { + return F("application/javascript"); + } else if(filename.endsWith(F(".png"))) { + return F("image/png"); + } else if(filename.endsWith(F(".gif"))) { + return F("image/gif"); + } else if(filename.endsWith(F(".jpg"))) { + return F("image/jpeg"); + } else if(filename.endsWith(F(".ico"))) { + return F("image/x-icon"); + } else if(filename.endsWith(F(".xml"))) { + return F("text/xml"); + } else if(filename.endsWith(F(".pdf"))) { + return F("application/x-pdf"); + } else if(filename.endsWith(F(".zip"))) { + return F("application/x-zip"); + } else if(filename.endsWith(F(".gz"))) { + return F("application/x-gzip"); + } + return F("text/plain"); +} + +String urldecode(String str) +{ + String encodedString = ""; + char c; + char code0; + char code1; + for(int i = 0; i < str.length(); i++) { + c = str.charAt(i); + if(c == '+') { + encodedString += ' '; + } else if(c == '%') { + char buffer[3]; + i++; + buffer[0] = str.charAt(i); + i++; + buffer[1] = str.charAt(i); + buffer[2] = '\0'; + c = (char)strtol((const char *)&buffer, NULL, 16); + encodedString += c; + } else { + encodedString += c; + } + yield(); + } + return encodedString; +} + +bool handleFileRead(String path) +{ + path = urldecode(path).substring(0, 31); + if(!httpIsAuthenticated(path)) return false; + + if(path.endsWith("/")) { + path += F("index.htm"); + } + String pathWithGz = path + F(".gz"); + if(filesystem->exists(pathWithGz) || filesystem->exists(path)) { + if(filesystem->exists(pathWithGz)) path += F(".gz"); + + File file = filesystem->open(path, "r"); + String contentType = getContentType(path); + if(path == F("/edit.htm.gz")) { + contentType = F("text/html"); + } + webServer.streamFile(file, contentType); + file.close(); + return true; + } + return false; +} + +void handleFileUpload() +{ + if(webServer.uri() != "/edit") { + return; + } + HTTPUpload & upload = webServer.upload(); + if(upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if(!filename.startsWith("/")) { + filename = "/" + filename; + } + debugPrintln(String(F("handleFileUpload Name: ")) + filename); + fsUploadFile = filesystem->open(filename, "w"); + filename.clear(); + } else if(upload.status == UPLOAD_FILE_WRITE) { + // DBG_OUTPUT_PORT.print("handleFileUpload Data: "); debugPrintln(upload.currentSize); + if(fsUploadFile) { + fsUploadFile.write(upload.buf, upload.currentSize); + } + } else if(upload.status == UPLOAD_FILE_END) { + if(fsUploadFile) { + fsUploadFile.close(); + } + debugPrintln(String(F("handleFileUpload Size: ")) + String(upload.totalSize)); + String filename = upload.filename; + if(filename.endsWith(".zi")) webHandleHaspConfig(); + } +} + +void handleFileDelete() +{ + if(webServer.args() == 0) { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + } + String path = webServer.arg(0); + debugPrintln(String(F("handleFileDelete: ")) + path); + if(path == "/") { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD PATH")); + } + if(!filesystem->exists(path)) { + return webServer.send(404, PSTR("text/plain"), PSTR("FileNotFound")); + } + filesystem->remove(path); + webServer.send(200, PSTR("text/plain"), ""); + path.clear(); +} + +void handleFileCreate() +{ + if(webServer.args() == 0) { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + } + String path = webServer.arg(0); + debugPrintln(String(F("handleFileCreate: ")) + path); + if(path == "/") { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD PATH")); + } + if(filesystem->exists(path)) { + return webServer.send(500, PSTR("text/plain"), PSTR("FILE EXISTS")); + } + File file = filesystem->open(path, "w"); + if(file) { + file.close(); + } else { + return webServer.send(500, PSTR("text/plain"), PSTR("CREATE FAILED")); + } + webServer.send(200, PSTR("text/plain"), ""); + path.clear(); +} + +void handleFileList() +{ + if(!webServer.hasArg(F("dir"))) { + webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + return; + } + + String path = webServer.arg(F("dir")); + debugPrintln(String(F("handleFileList: ")) + path); + path.clear(); + +#if defined(ARDUINO_ARCH_ESP32) + debugPrintln(PSTR("HTTP: Listing files on the internal flash:")); + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + String output = "["; + + while(file) { + if(output != "[") { + output += ','; + } + bool isDir = false; + output += F("{\"type\":\""); + output += (isDir) ? F("dir") : F("file"); + output += F("\",\"name\":\""); + if(file.name()[0] == '/') { + output += &(file.name()[1]); + } else { + output += file.name(); + } + output += F("\"}"); + + char msg[64]; + sprintf(msg, PSTR("HTTP: * %s (%u bytes)"), file.name(), (uint32_t)file.size()); + debugPrintln(msg); + + // file.close(); + file = root.openNextFile(); + } + output += "]"; +#else + Dir dir = filesystem->openDir(path); + String output = "["; + while(dir.next()) { + File entry = dir.openFile("r"); + if(output != "[") { + output += ','; + } + bool isDir = false; + output += F("{\"type\":\""); + output += (isDir) ? F("dir") : F("file"); + output += F("\",\"name\":\""); + if(entry.name()[0] == '/') { + output += &(entry.name()[1]); + } else { + output += entry.name(); + } + output += F("\"}"); + entry.close(); + } + output += "]"; +#endif + webServer.send(200, PSTR("text/json"), output); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleConfig() +{ // http://plate01/config + if(!httpIsAuthenticated(F("/config"))) return; + + if(webServer.method() == HTTP_POST) { + if(webServer.hasArg(F("save"))) { + DynamicJsonDocument settings(256); + + if(webServer.arg(F("save")) == String(F("hasp"))) { + /* + if(webServer.hasArg(F_CONFIG_THEME) settings[F_CONFIG_THEME] = webServer.arg(F_CONFIG_THEME).toInt(); + if(webServer.hasArg(F_CONFIG_HUE) settings[F_CONFIG_HUE] = webServer.arg(F_CONFIG_HUE.toInt(); + if(webServer.hasArg(F_CONFIG_ZIFONT) settings[F_CONFIG_ZIFONT] = webServer.arg(F_CONFIG_ZIFONT; + if(webServer.hasArg(F_CONFIG_PAGES) settings[F_CONFIG_PAGES] = webServer.arg(F_CONFIG_PAGES; + if(webServer.hasArg(F_CONFIG_STARTPAGE) + settings[F_CONFIG_STARTPAGE] = webServer.arg(F_CONFIG_STARTPAGE).toInt();*/ + for(int i = 0; i < webServer.args(); i++) settings[webServer.argName(i)] = webServer.arg(i); + + haspSetConfig(settings.as()); + + } else if(webServer.arg(F("save")) == String(F("mqtt"))) { + } else if(webServer.arg(F("save")) == String(F("http"))) { + } else if(webServer.arg(F("save")) == String(F("wifi"))) { + } + } + } + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += F("
"); + httpMessage += + F("

"); + +#if LV_USE_HASP_MQTT > 0 + httpMessage += + F("

"); +#endif + + httpMessage += + F("

"); + + httpMessage += + F("

"); + + httpMessage += + F("

"); + + httpMessage += + F("

"); + + httpMessage += F("

"); + + httpMessage += F("
"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +#if LV_USE_HASP_MQTT > 0 +void webHandleMqttConfig() +{ // http://plate01/config/mqtt + if(!httpIsAuthenticated(F("/config/mqtt"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += String(F("
")); + httpMessage += String(F("WiFi SSID (required)"; + httpMessage += String(F("
WiFi Password (required)"; + httpMessage += + F("

HASP Node Name (required. lowercase letters, numbers, and _ only)" + ""; + httpMessage += F("

Group Name (required)"; + httpMessage += F("

MQTT Broker (required)"; + httpMessage += F("
MQTT Port (required)"; + httpMessage += F("
MQTT User (optional)"; + httpMessage += F("
MQTT Password (optional)

HASP Admin Username (optional)"; + httpMessage += + String(F("
HASP Admin Password (optional)

Motion Sensor Pin: ")); + + httpMessage += String(F("
Serial debug output enabled:
Telnet debug output enabled:
mDNS enabled:")); + /*if (mdnsEnabled) + { + httpMessage += String(F(" checked='checked'")); + } + httpMessage += String(F(">

")); + + if (updateEspAvailable) + { + httpMessage += String(F("

HASP Update + available!

")); httpMessage += String(F("
")); + httpMessage += String(F(""; httpMessage += String(F("
")); + }*/ + + httpMessage += F(""); + + httpMessage += F("

"); + + httpMessage += F("
"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////// +#if LV_USE_HASP_MQTT > 0 +void webHandleHaspConfig() +{ // http://plate01/config/http + if(!httpIsAuthenticated(F("/config/hasp"))) return; + + DynamicJsonDocument settings(256); + haspGetConfig(settings.to()); + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += String(F("

")); + httpMessage += F("

"); + + httpMessage += String(F("
")); + httpMessage += String(F("

UI Theme (required)
")); + httpMessage += + "Hue

"; + + httpMessage += String(F("

Default Font

")); + + httpMessage += String(F("

Pages File (required)"; + httpMessage += String(F("
Startup Page (required)

"; + + httpMessage += F("
"); + + httpMessage += F("
"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleNotFound() +{ // webServer 404 + if(handleFileRead(webServer.uri())) return; + + debugPrintln(String(F("HTTP: Sending 404 to client connected from: ")) + webServer.client().remoteIP().toString()); + + String httpMessage((char *)0); + httpMessage.reserve(128); + httpMessage += F("File Not Found\n\nURI: "); + httpMessage += webServer.uri(); + httpMessage += F("\nMethod: "); + httpMessage += (webServer.method() == HTTP_GET) ? F("GET") : F("POST"); + httpMessage += F("\nArguments: "); + httpMessage += webServer.args(); + httpMessage += "\n"; + for(uint8_t i = 0; i < webServer.args(); i++) { + httpMessage += " " + webServer.argName(i) + ": " + webServer.arg(i) + "\n"; + } + webServer.send(404, PSTR("text/plain"), httpMessage.c_str()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleSaveConfig() +{ + if(!httpIsAuthenticated(F("/saveConfig"))) return; + + configWriteConfig(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleFirmware() +{ + if(!httpIsAuthenticated(F("/firmware"))) return; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleEspFirmware() +{ // http://plate01/espfirmware + if(!httpIsAuthenticated(F("/espfirmware"))) return; + + String nodename = haspGetNodename(); + char buffer[64]; + String httpMessage((char *)0); + httpMessage.reserve(128); + httpMessage += String(F("

")); + httpMessage += String(haspGetNodename()); + httpMessage += String(F(" ESP update


Updating ESP firmware from: ")); + httpMessage += String(webServer.arg("espFirmware")); + + webSendPage(nodename, httpMessage.length(), true); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + + debugPrintln(String(F("HTTP: Attempting ESP firmware update from: ")) + String(webServer.arg("espFirmware"))); + // espStartOta(webServer.arg("espFirmware")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleReboot() +{ // http://plate01/reboot + if(!httpIsAuthenticated(F("/reboot"))) return; + + String nodename = haspGetNodename(); + String httpMessage = F("Rebooting Device"); + webSendPage(nodename, httpMessage.length(), true); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + delay(500); + + debugPrintln(PSTR("HTTP: Reboot device")); + haspSetPage(0); + haspSetAttr(F("p[0].b[1].txt"), F("\"Rebooting...\"")); + + delay(500); + haspReset(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleResetConfig() +{ // http://plate01/resetConfig + if(!httpIsAuthenticated(F("/resetConfig"))) return; + + bool resetConfirmed = webServer.arg(F("confirm")) == F("yes"); + String nodename = haspGetNodename(); + char buffer[64]; + String httpMessage((char *)0); + httpMessage.reserve(128); + + if(resetConfirmed) { // User has confirmed, so reset everything + httpMessage += F("

"); + httpMessage += haspGetNodename(); + bool formatted = SPIFFS.format(); + if(formatted) { + httpMessage += F("

Resetting all saved settings and restarting device into WiFi AP mode"); + } else { + httpMessage += F("Failed to format the internal flash partition"); + resetConfirmed = false; + } + } else { + httpMessage += + F("

Warning

This process will reset all settings to the default values. The internal flash will " + "be erased and the device is restarted. You may need to connect to the WiFi AP displayed on the panel to " + "re-configure the device before accessing it again. ALL FILES WILL BE LOST!" + "


" + "

" + "


" + "
"); + } + + webSendPage(nodename, httpMessage.length(), resetConfirmed); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + + if(resetConfirmed) { + delay(1000); + // configClearSaved(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpSetup(const JsonObject & settings) +{ + webServer.on(F("/page/"), []() { + String pageid = webServer.arg(F("page")); + webServer.send(200, PSTR("text/plain"), "Page: '" + pageid + "'"); + haspSetPage(pageid.toInt()); + }); + + webServer.on("/list", HTTP_GET, handleFileList); + // load editor + webServer.on("/edit", HTTP_GET, []() { + if(!handleFileRead("/edit.htm")) { + webServer.send(404, "text/plain", "FileNotFound"); + } + }); + webServer.on("/edit", HTTP_PUT, handleFileCreate); + webServer.on("/edit", HTTP_DELETE, handleFileDelete); + // first callback is called after the request has ended with all parsed arguments + // second callback handles file uploads at that location + webServer.on("/edit", HTTP_POST, []() { webServer.send(200, "text/plain", ""); }, handleFileUpload); + // get heap status, analog input value and all GPIO statuses in one json call + webServer.on("/all", HTTP_GET, []() { + String json('{'); + json += "\"heap\":" + String(ESP.getFreeHeap()); + json += ", \"analog\":" + String(analogRead(A0)); + json += "}"; + webServer.send(200, "text/json", json); + json.clear(); + }); + + webServer.on(F("/"), webHandleRoot); + webServer.on(F("/about"), webHandleAbout); + webServer.on(F("/info"), webHandleInfo); + webServer.on(F("/config"), webHandleConfig); + webServer.on(F("/config/hasp"), webHandleHaspConfig); +#if LV_USE_HASP_MQTT > 0 + webServer.on(F("/config/mqtt"), webHandleMqttConfig); +#endif + webServer.on(F("/saveConfig"), webHandleSaveConfig); + webServer.on(F("/resetConfig"), httpHandleResetConfig); + webServer.on(F("/firmware"), webHandleFirmware); + webServer.on(F("/espfirmware"), httpHandleEspFirmware); + webServer.on(F("/reboot"), httpHandleReboot); + webServer.onNotFound(httpHandleNotFound); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpReconnect() +{ + if(!httpEnable) return; + + webServer.stop(); + webServer.begin(); + debugPrintln(String(F("HTTP: Server started @ http://")) + WiFi.localIP().toString()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpLoop(bool wifiIsConnected) +{ + if(httpEnable) webServer.handleClient(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool httpGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_HTTP_ENABLE] == httpEnable && settings[F_HTTP_PORT] == httpPort && + settings[F_HTTP_USER] == httpUser && settings[F_HTTP_PASS] == httpPassword) + return false; + + settings[F_HTTP_ENABLE] = httpEnable; + settings[F_HTTP_PORT] = httpPort; + settings[F_HTTP_USER] = httpUser; + settings[F_HTTP_PASS] = httpPassword; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/hasp_http.old b/src/hasp_http.old new file mode 100644 index 00000000..1128bde8 --- /dev/null +++ b/src/hasp_http.old @@ -0,0 +1,863 @@ +//#include "webServer.h" +#include +#include "ArduinoJson.h" + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_http.h" +#include "hasp_mqtt.h" +#include "hasp_wifi.h" +#include "hasp_config.h" +#include "hasp.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include "SPIFFS.h" +#endif +#include +#include + +bool httpEnable = true; +bool webServerStarted = false; +uint16_t httpPort = 80; +FS * filesystem = &SPIFFS; +File fsUploadFile; +String httpUser = "admin"; +String httpPassword = ""; + +#if defined(ARDUINO_ARCH_ESP8266) +#include +ESP8266WebServer webServer(80); +#endif + +#if defined(ARDUINO_ARCH_ESP32) +#include // needed to get the ResetInfo +#include +WebServer webServer(80); + +// Compatibility function for ESP8266 getRestInfo +String esp32ResetReason(uint8_t cpuid) +{ + if(cpuid > 1) { + return F("Invalid CPU id"); + } + RESET_REASON reason = rtc_get_reset_reason(cpuid); + + switch(reason) { + case 1: + return F("POWERON_RESET"); + break; /**<1, Vbat power on reset*/ + case 3: + return F("SW_RESET"); + break; /**<3, Software reset digital core*/ + case 4: + return F("OWDT_RESET"); + break; /**<4, Legacy watch dog reset digital core*/ + case 5: + return F("DEEPSLEEP_RESET"); + break; /**<5, Deep Sleep reset digital core*/ + case 6: + return F("SDIO_RESET"); + break; /**<6, Reset by SLC module, reset digital core*/ + case 7: + return F("TG0WDT_SYS_RESET"); + break; /**<7, Timer Group0 Watch dog reset digital core*/ + case 8: + return F("TG1WDT_SYS_RESET"); + break; /**<8, Timer Group1 Watch dog reset digital core*/ + case 9: + return F("RTCWDT_SYS_RESET"); + break; /**<9, RTC Watch dog Reset digital core*/ + case 10: + return F("INTRUSION_RESET"); + break; /**<10, Instrusion tested to reset CPU*/ + case 11: + return F("TGWDT_CPU_RESET"); + break; /**<11, Time Group reset CPU*/ + case 12: + return F("SW_CPU_RESET"); + break; /**<12, Software reset CPU*/ + case 13: + return F("RTCWDT_CPU_RESET"); + break; /**<13, RTC Watch dog Reset CPU*/ + case 14: + return F("EXT_CPU_RESET"); + break; /**<14, for APP CPU, reseted by PRO CPU*/ + case 15: + return F("RTCWDT_BROWN_OUT_RESET"); + break; /**<15, Reset when the vdd voltage is not stable*/ + case 16: + return F("RTCWDT_RTC_RESET"); + break; /**<16, RTC Watch dog reset digital core and rtc module*/ + default: + return F("NO_MEAN"); + } +} + +// these need to be removed +const uint8_t D0 = 0; +const uint8_t D1 = 1; +const uint8_t D2 = 2; +#endif // ESP32 + +static const char HTTP_DOCTYPE[] PROGMEM = + ""; +static const char HTTP_META_GO_BACK[] PROGMEM = ""; +static const char HTTP_HEADER[] PROGMEM = "%s"; +static const char HTTP_STYLE[] PROGMEM = + ""; +static const char HTTP_SCRIPT[] PROGMEM = ""; +static const char HTTP_HEADER_END[] PROGMEM = + "
"; +static const char HTTP_END[] PROGMEM = ""; +// Additional CSS style to match Hass theme +static const char HASP_STYLE[] PROGMEM = + ""; + +// these need to be removed +uint8_t motionPin = 0; // GPIO input pin for motion sensor if connected and enabled +bool debugSerialEnabled = true; // Enable USB serial debug output +bool debugTelnetEnabled = false; // Enable telnet debug output + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// These defaults may be overwritten with values saved by the web interface +char motionPinConfig[3] = "0"; +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// URL for auto-update "version.json" +const char UPDATE_URL[] = "http://haswitchplate.com/update/version.json"; +// Default link to compiled Arduino firmware image +String espFirmwareUrl = "http://haswitchplate.com/update/HASwitchPlate.ino.d1_mini.bin"; +// Default link to compiled Nextion firmware images +String lcdFirmwareUrl = "http://haswitchplate.com/update/HASwitchPlate.tft"; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +String formatBytes(size_t bytes); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool httpIsAuthenticated(const String & page) +{ + if(httpPassword[0] != '\0') { // Request HTTP auth if httpPassword is set + if(!webServer.authenticate(httpUser.c_str(), httpPassword.c_str())) { + webServer.requestAuthentication(); + return false; + } + } + char buffer[128]; + sprintf(buffer, PSTR("HTTP: Sending %s page to client connected from: %s"), page.c_str(), + webServer.client().remoteIP().toString().c_str()); + debugPrintln(buffer); + return true; +} + +void webSendPage(String & nodename, uint32_t httpdatalength, bool gohome = false) +{ + char buffer[64]; + + /* Calculate Content Length upfront */ + uint16_t contentLength = 0; + contentLength += sizeof(HTTP_DOCTYPE) - 1; + contentLength += sizeof(HTTP_HEADER) - 1 - 2 + nodename.length(); + contentLength += sizeof(HTTP_SCRIPT) - 1; + contentLength += sizeof(HTTP_STYLE) - 1; + contentLength += sizeof(HASP_STYLE) - 1; + if(gohome) contentLength += sizeof(HTTP_META_GO_BACK) - 1; + contentLength += sizeof(HTTP_HEADER_END) - 1; + contentLength += sizeof(HTTP_END) - 1; + + webServer.setContentLength(contentLength + httpdatalength); + + webServer.send_P(200, PSTR("text/html"), HTTP_DOCTYPE); // 122 + sprintf_P(buffer, HTTP_HEADER, nodename.c_str()); + webServer.sendContent(buffer); // 17-2+len + webServer.sendContent_P(HTTP_SCRIPT); // 131 + webServer.sendContent_P(HTTP_STYLE); // 487 + webServer.sendContent_P(HASP_STYLE); // 145 + if(gohome) webServer.sendContent_P(HTTP_META_GO_BACK); // 47 + webServer.sendContent_P(HTTP_HEADER_END); // 80 +} + +void webHandleRoot() +{ + if(!httpIsAuthenticated(F("root"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += String(F("

")); + httpMessage += String(nodename); + httpMessage += String(F("

")); + + httpMessage += F("

"); + httpMessage += F("

"); + + httpMessage += + F("

"); + httpMessage += + F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleAbout() +{ // http://plate01/about + if(!httpIsAuthenticated(F("/about"))) return; + + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1250); + + httpMessage += F("

HASP OpenHardware edition

Copyright© 2020 Francis Van Roie " + "
MIT License

"); + httpMessage += F("

Based on the previous work of the following open source developers.


"); + httpMessage += + F("

HASwitchPlate

Copyright© 2019 Allen Derusha allen@derusha.org
MIT License

"); + httpMessage += + F("

LittlevGL

Copyright© 2016 Gábor Kiss-Vámosi
Copyright© 2019 " + "LittlevGL
MIT License

"); + httpMessage += F("

Lvgl ziFont Font Engine

Copyright© 2020 Francis Van Roie
MIT License

"); + httpMessage += F("

TFT_eSPI Library

Copyright© 2017 Bodmer (https://github.com/Bodmer) All " + "rights reserved.
FreeBSD License
"); + httpMessage += + F("includes parts from the Adafruit_GFX library - Copyright© 2012 Adafruit Industries. All rights " + "reserved. BSD License

"); + httpMessage += F("

ArduinoJson

Copyright© 2014-2019 Benoit BLANCHON
MIT License

"); + httpMessage += F("

PubSubClient

Copyright© 2008-2015 Nicholas O'Leary
MIT License

"); + httpMessage += F("

Syslog

Copyright© 2016 Martin Sloup
MIT License

"); + + httpMessage += F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleInfo() +{ // http://plate01/ + if(!httpIsAuthenticated(F("/info"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += F("
MQTT Status: "); + if(mqttIsConnected()) { // Check MQTT connection + httpMessage += String(F("Connected")); + } else { + httpMessage += String(F("Disconnected, return code: ")); + // +String(mqttClient.returnCode()); + } + httpMessage += String(F("
MQTT ClientID: ")); + // +String(mqttClientId); + httpMessage += F("
HASP Version: "); + httpMessage += String(haspGetVersion()); + httpMessage += F("
Uptime: "); + httpMessage += String(long(millis() / 1000)); + + // httpMessage += String(F("
LCD Model: ")) + String(LV_HASP_HOR_RES_MAX) + " x " + + // String(LV_HASP_VER_RES_MAX); httpMessage += String(F("
LCD Version: ")) + String(lcdVersion); + httpMessage += F("

LCD Active Page: "); + httpMessage += String(haspGetPage()); + httpMessage += F("
CPU Frequency: "); + httpMessage += String(ESP.getCpuFreqMHz()); + + httpMessage += F("MHz

SSID: "); + httpMessage += String(WiFi.SSID()); + httpMessage += F("
Signal Strength: "); + httpMessage += String(WiFi.RSSI()); + httpMessage += F("
IP Address: "); + httpMessage += String(WiFi.localIP().toString()); + httpMessage += F("
Gateway: "); + httpMessage += String(WiFi.gatewayIP().toString()); + httpMessage += F("
DNS Server: "); + httpMessage += String(WiFi.dnsIP().toString()); + httpMessage += F("
MAC Aress: "); + httpMessage += String(WiFi.macAddress()); + + httpMessage += F("

ESP Chip Id: "); +#if defined(ARDUINO_ARCH_ESP32) + httpMessage += String(ESP.getChipRevision()); +#else + httpMessage += String(ESP.getChipId()); +#endif + httpMessage += F("
Flash Chip Size: "); + httpMessage += formatBytes(ESP.getFlashChipSize()); + httpMessage += F("
Program Size: "); + httpMessage += formatBytes(ESP.getSketchSize()); + httpMessage += F(" bytes
Free Program Space: "); + httpMessage += formatBytes(ESP.getFreeSketchSpace()); + httpMessage += F(" bytes
Free Memory: "); + httpMessage += formatBytes(ESP.getFreeHeap()); + +#if defined(ARDUINO_ARCH_ESP32) + // httpMessage += F("
Heap Max Alloc: "); + // httpMessage += String(ESP.getMaxAllocHeap()); + httpMessage += F("
Memory Fragmentation: "); + httpMessage += String((int16_t)(100.00f - (float)ESP.getMaxAllocHeap() / (float)ESP.getFreeHeap() * 100.00f)); + httpMessage += F("
ESP SDK version: "); + httpMessage += String(ESP.getSdkVersion()); + httpMessage += F("
Last Reset: CPU0: "); + httpMessage += String(esp32ResetReason(0)); + httpMessage += F(" / CPU1: "); + httpMessage += String(esp32ResetReason(1)); +#else + httpMessage += F("
Memory Fragmentation: "); + httpMessage += String(ESP.getHeapFragmentation()); + httpMessage += F("
ESP Core version: "); + httpMessage += String(ESP.getCoreVersion()); + httpMessage += F("
Last Reset: "); + httpMessage += String(ESP.getResetInfo()); +#endif + + httpMessage += F("

"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +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"; + } +} + +String getContentType(String filename) +{ + if(webServer.hasArg(F("download"))) { + return F("application/octet-stream"); + } else if(filename.endsWith(F(".htm"))) { + return F("text/html"); + } else if(filename.endsWith(F(".html"))) { + return F("text/html"); + } else if(filename.endsWith(F(".css"))) { + return F("text/css"); + } else if(filename.endsWith(F(".js"))) { + return F("application/javascript"); + } else if(filename.endsWith(F(".png"))) { + return F("image/png"); + } else if(filename.endsWith(F(".gif"))) { + return F("image/gif"); + } else if(filename.endsWith(F(".jpg"))) { + return F("image/jpeg"); + } else if(filename.endsWith(F(".ico"))) { + return F("image/x-icon"); + } else if(filename.endsWith(F(".xml"))) { + return F("text/xml"); + } else if(filename.endsWith(F(".pdf"))) { + return F("application/x-pdf"); + } else if(filename.endsWith(F(".zip"))) { + return F("application/x-zip"); + } else if(filename.endsWith(F(".gz"))) { + return F("application/x-gzip"); + } + return F("text/plain"); +} + +String urldecode(String str) +{ + String encodedString = ""; + char c; + char code0; + char code1; + for(int i = 0; i < str.length(); i++) { + c = str.charAt(i); + if(c == '+') { + encodedString += ' '; + } else if(c == '%') { + char buffer[3]; + i++; + buffer[0] = str.charAt(i); + i++; + buffer[1] = str.charAt(i); + buffer[2] = '\0'; + c = (char)strtol((const char *)&buffer, NULL, 16); + encodedString += c; + } else { + encodedString += c; + } + yield(); + } + return encodedString; +} + +bool handleFileRead(String path) +{ + path = urldecode(path).substring(0, 31); + if(!httpIsAuthenticated(path)) return false; + + if(path.endsWith("/")) { + path += F("index.htm"); + } + String pathWithGz = path + F(".gz"); + if(filesystem->exists(pathWithGz) || filesystem->exists(path)) { + if(filesystem->exists(pathWithGz)) path += F(".gz"); + + File file = filesystem->open(path, "r"); + String contentType = getContentType(path); + if(path == F("/edit.htm.gz")) { + contentType = F("text/html"); + } + webServer.streamFile(file, contentType); + file.close(); + return true; + } + return false; +} + +void handleFileUpload() +{ + if(webServer.uri() != "/edit") { + return; + } + HTTPUpload & upload = webServer.upload(); + if(upload.status == UPLOAD_FILE_START) { + String filename = upload.filename; + if(!filename.startsWith("/")) { + filename = "/" + filename; + } + debugPrintln(String(F("handleFileUpload Name: ")) + filename); + fsUploadFile = filesystem->open(filename, "w"); + filename.clear(); + } else if(upload.status == UPLOAD_FILE_WRITE) { + // DBG_OUTPUT_PORT.print("handleFileUpload Data: "); debugPrintln(upload.currentSize); + if(fsUploadFile) { + fsUploadFile.write(upload.buf, upload.currentSize); + } + } else if(upload.status == UPLOAD_FILE_END) { + if(fsUploadFile) { + fsUploadFile.close(); + } + debugPrintln(String(F("handleFileUpload Size: ")) + String(upload.totalSize)); + } +} + +void handleFileDelete() +{ + if(webServer.args() == 0) { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + } + String path = webServer.arg(0); + debugPrintln(String(F("handleFileDelete: ")) + path); + if(path == "/") { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD PATH")); + } + if(!filesystem->exists(path)) { + return webServer.send(404, PSTR("text/plain"), PSTR("FileNotFound")); + } + filesystem->remove(path); + webServer.send(200, PSTR("text/plain"), ""); + path.clear(); +} + +void handleFileCreate() +{ + if(webServer.args() == 0) { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + } + String path = webServer.arg(0); + debugPrintln(String(F("handleFileCreate: ")) + path); + if(path == "/") { + return webServer.send(500, PSTR("text/plain"), PSTR("BAD PATH")); + } + if(filesystem->exists(path)) { + return webServer.send(500, PSTR("text/plain"), PSTR("FILE EXISTS")); + } + File file = filesystem->open(path, "w"); + if(file) { + file.close(); + } else { + return webServer.send(500, PSTR("text/plain"), PSTR("CREATE FAILED")); + } + webServer.send(200, PSTR("text/plain"), ""); + path.clear(); +} + +void handleFileList() +{ + if(!webServer.hasArg(F("dir"))) { + webServer.send(500, PSTR("text/plain"), PSTR("BAD ARGS")); + return; + } + + String path = webServer.arg(F("dir")); + debugPrintln(String(F("handleFileList: ")) + path); + path.clear(); + +#if defined(ARDUINO_ARCH_ESP32) + debugPrintln(PSTR("HTTP: Listing files on the internal flash:")); + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + String output = "["; + + while(file) { + if(output != "[") { + output += ','; + } + bool isDir = false; + output += F("{\"type\":\""); + output += (isDir) ? F("dir") : F("file"); + output += F("\",\"name\":\""); + if(file.name()[0] == '/') { + output += &(file.name()[1]); + } else { + output += file.name(); + } + output += F("\"}"); + + char msg[64]; + sprintf(msg, PSTR("HTTP: * %s (%u bytes)"), file.name(), (uint32_t)file.size()); + debugPrintln(msg); + + // file.close(); + file = root.openNextFile(); + } + output += "]"; +#else + Dir dir = filesystem->openDir(path); + String output = "["; + while(dir.next()) { + File entry = dir.openFile("r"); + if(output != "[") { + output += ','; + } + bool isDir = false; + output += F("{\"type\":\""); + output += (isDir) ? F("dir") : F("file"); + output += F("\",\"name\":\""); + if(entry.name()[0] == '/') { + output += &(entry.name()[1]); + } else { + output += entry.name(); + } + output += F("\"}"); + entry.close(); + } + output += "]"; +#endif + webServer.send(200, PSTR("text/json"), output); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleConfig() +{ // http://plate01/ + if(!httpIsAuthenticated(F("/config"))) return; + + char buffer[64]; + String nodename = haspGetNodename(); + String httpMessage((char *)0); + httpMessage.reserve(1024); + + httpMessage += String(F("
")); + httpMessage += String(F("WiFi SSID (required)"; + httpMessage += String(F("
WiFi Password (required)"; + httpMessage += + F("

HASP Node Name (required. lowercase letters, numbers, and _ only)" + ""; + httpMessage += F("

Group Name (required)"; + httpMessage += F("

MQTT Broker (required)"; + httpMessage += F("
MQTT Port (required)"; + httpMessage += F("
MQTT User (optional)"; + httpMessage += F("
MQTT Password (optional)

HASP Admin Username (optional)"; + httpMessage += + String(F("
HASP Admin Password (optional)

Motion Sensor Pin: ")); + + httpMessage += String(F("
Serial debug output enabled:
Telnet debug output enabled:
mDNS enabled:

")); + + if (updateEspAvailable) + { + httpMessage += String(F("

HASP Update + available!

")); httpMessage += String(F("
")); + httpMessage += String(F(""; httpMessage += String(F("
")); + }*/ + + httpMessage += F("
"); + + httpMessage += F("

"); + + httpMessage += F("
"); + + webSendPage(nodename, httpMessage.length(), false); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleNotFound() +{ // webServer 404 + if(handleFileRead(webServer.uri())) return; + + debugPrintln(String(F("HTTP: Sending 404 to client connected from: ")) + webServer.client().remoteIP().toString()); + + String httpMessage((char *)0); + httpMessage.reserve(128); + httpMessage += F("File Not Found\n\nURI: "); + httpMessage += webServer.uri(); + httpMessage += F("\nMethod: "); + httpMessage += (webServer.method() == HTTP_GET) ? F("GET") : F("POST"); + httpMessage += F("\nArguments: "); + httpMessage += webServer.args(); + httpMessage += "\n"; + for(uint8_t i = 0; i < webServer.args(); i++) { + httpMessage += " " + webServer.argName(i) + ": " + webServer.arg(i) + "\n"; + } + webServer.send(404, PSTR("text/plain"), httpMessage.c_str()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleSaveConfig() +{ + if(!httpIsAuthenticated(F("/saveconfig"))) return; + + configWriteConfig(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void webHandleFirmware() +{ + if(!httpIsAuthenticated(F("/firmware"))) return; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleEspFirmware() +{ // http://plate01/espfirmware + if(!httpIsAuthenticated(F("/espfirmware"))) return; + + String nodename = haspGetNodename(); + char buffer[64]; + String httpMessage((char *)0); + httpMessage.reserve(128); + httpMessage += String(F("

")); + httpMessage += String(haspGetNodename()); + httpMessage += String(F(" ESP update


Updating ESP firmware from: ")); + httpMessage += String(webServer.arg("espFirmware")); + + webSendPage(nodename, httpMessage.length(), true); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + + debugPrintln(String(F("HTTP: Attempting ESP firmware update from: ")) + String(webServer.arg("espFirmware"))); + // espStartOta(webServer.arg("espFirmware")); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleReboot() +{ // http://plate01/reboot + if(!httpIsAuthenticated(F("/reboot"))) return; + + String nodename = haspGetNodename(); + String httpMessage = F("Rebooting Device"); + webSendPage(nodename, httpMessage.length(), true); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + delay(500); + + debugPrintln(PSTR("HTTP: Reboot device")); + haspSetPage(0); + haspSetAttr(F("p[0].b[1].txt"), F("\"Rebooting...\"")); + + delay(500); + haspReset(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpHandleResetConfig() +{ // http://plate01/resetConfig + if(!httpIsAuthenticated(F("/espfirmware"))) return; + + bool resetConfirmed = webServer.arg(F("confirm")) == F("yes"); + String nodename = haspGetNodename(); + char buffer[64]; + String httpMessage((char *)0); + httpMessage.reserve(128); + + if(resetConfirmed) { // User has confirmed, so reset everything + httpMessage += F("

"); + httpMessage += haspGetNodename(); + httpMessage += F("

Resetting all saved settings and restarting device into WiFi AP mode"); + } else { + httpMessage += F("

Warning

This process will reset all settings to the default values and " + "restart the device. You may need to connect to the WiFi AP displayed on the panel to " + "re-configure the device before accessing it again." + "


" + "

" + "


" + "
"); + } + + webSendPage(nodename, httpMessage.length(), resetConfirmed); + webServer.sendContent(httpMessage); // len + webServer.sendContent_P(HTTP_END); // 20 + + if(resetConfirmed) { + delay(1000); + // configClearSaved(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpSetup(const JsonObject & settings) +{ + webServer.on(F("/page/"), []() { + String pageid = webServer.arg(F("page")); + webServer.send(200, PSTR("text/plain"), "Page: '" + pageid + "'"); + haspSetPage(pageid.toInt()); + }); + + webServer.on("/list", HTTP_GET, handleFileList); + // load editor + webServer.on("/edit", HTTP_GET, []() { + if(!handleFileRead("/edit.htm")) { + webServer.send(404, "text/plain", "FileNotFound"); + } + }); + webServer.on("/edit", HTTP_PUT, handleFileCreate); + webServer.on("/edit", HTTP_DELETE, handleFileDelete); + // first callback is called after the request has ended with all parsed arguments + // second callback handles file uploads at that location + webServer.on("/edit", HTTP_POST, []() { webServer.send(200, "text/plain", ""); }, handleFileUpload); + // get heap status, analog input value and all GPIO statuses in one json call + webServer.on("/all", HTTP_GET, []() { + String json('{'); + json += "\"heap\":" + String(ESP.getFreeHeap()); + json += ", \"analog\":" + String(analogRead(A0)); + json += "}"; + webServer.send(200, "text/json", json); + json.clear(); + }); + + webServer.on(F("/"), webHandleRoot); + webServer.on(F("/about"), webHandleAbout); + webServer.on(F("/info"), webHandleInfo); + webServer.on(F("/config"), webHandleConfig); + webServer.on(F("/saveConfig"), webHandleSaveConfig); + webServer.on(F("/resetConfig"), httpHandleResetConfig); + webServer.on(F("/firmware"), webHandleFirmware); + webServer.on(F("/espfirmware"), httpHandleEspFirmware); + webServer.on(F("/reboot"), httpHandleReboot); + webServer.onNotFound(httpHandleNotFound); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpReconnect() +{ + if(!httpEnable) return; + + webServer.stop(); + webServer.begin(); + debugPrintln(String(F("HTTP: Server started @ http://")) + WiFi.localIP().toString()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +void httpLoop(bool wifiIsConnected) +{ + if(httpEnable) webServer.handleClient(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +bool httpGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_CONFIG_ENABLE] == httpEnable && settings[F_CONFIG_PORT] == httpPort && + settings[F_CONFIG_USER] == httpUser && settings[F_CONFIG_PASS] == httpPassword) + return false; + + settings[F_CONFIG_ENABLE] = httpEnable; + settings[F_CONFIG_PORT] = httpPort; + settings[F_CONFIG_USER] = httpUser; + settings[F_CONFIG_PASS] = httpPassword; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/hasp_log.cpp b/src/hasp_log.cpp new file mode 100644 index 00000000..c833a6d4 --- /dev/null +++ b/src/hasp_log.cpp @@ -0,0 +1,43 @@ +#include + +#ifdef ESP8266 +#include +#include +#else +#include +#endif +#include +#include + +#include "hasp_log.h" +#include "hasp_debug.h" + +void debugPrintln(String debugText) +{ + serialPrintln(debugText); + // if(WiFi.isConnected()) syslog.log(LOG_INFO, debugText); +} + +void errorPrintln(String debugText) +{ + char buffer[256]; + sprintf_P(buffer, debugText.c_str(), PSTR("[ERROR] ")); + serialPrintln(buffer); + if(WiFi.isConnected()) { + char buffer[256]; + sprintf_P(buffer, debugText.c_str(), ""); + // syslog.log(LOG_ERR, buffer); + } +} + +void warningPrintln(String debugText) +{ + char buffer[256]; + sprintf_P(buffer, debugText.c_str(), PSTR("[WARNING] ")); + serialPrintln(buffer); + if(WiFi.isConnected()) { + char buffer[256]; + sprintf_P(buffer, debugText.c_str(), ""); + // syslog.log(LOG_WARNING, buffer); + } +} \ No newline at end of file diff --git a/src/hasp_mdns.cpp b/src/hasp_mdns.cpp new file mode 100644 index 00000000..b8471c13 --- /dev/null +++ b/src/hasp_mdns.cpp @@ -0,0 +1,55 @@ +#include "Arduino.h" +#include "ArduinoJson.h" + +#ifdef ESP32 +#include +#else +#include +MDNSResponder::hMDNSService hMDNSService; +#endif + +#include "hasp_mdns.h" + +const char F_CONFIG_ENABLE[] PROGMEM = "enable"; + +bool mdnsEnabled = true; +String hasp2Node = "plate01"; +const float haspVersion = 0.38; + +void mdnsSetup(const JsonObject & settings) +{ + if(mdnsEnabled) { + // Setup mDNS service discovery if enabled + // MDNS.addService(String(hasp2Node), String("tcp"), 80); + /*if(debugTelnetEnabled) { + MDNS.addService(haspNode, "telnet", "tcp", 23); + }*/ + // MDNS.addServiceTxt(hasp2Node, "tcp", "app_name", "HASwitchPlate"); + // MDNS.addServiceTxt(hasp2Node, "tcp", "app_version", String(haspVersion).c_str()); + MDNS.begin(hasp2Node.c_str()); + } +} + +void mdnsLoop(bool wifiIsConnected) +{ + // if(mdnsEnabled) { + // MDNS(); + // }s +} + +void mdnsStop() +{ + MDNS.end(); +} + +bool mdnsGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_CONFIG_ENABLE] == mdnsEnabled) return false; + + settings[F_CONFIG_ENABLE] = mdnsEnabled; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/hasp_mqtt.cpp b/src/hasp_mqtt.cpp new file mode 100644 index 00000000..f0cdd117 --- /dev/null +++ b/src/hasp_mqtt.cpp @@ -0,0 +1,422 @@ +#include +#include "ArduinoJson.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include +#else +#include +#include +#include +#include +#endif +#include + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_config.h" +#include "hasp_mqtt.h" +#include "hasp_wifi.h" +#include "hasp.h" + +#include "user_config_override.h" + +// Size of buffer for incoming MQTT message +#define mqttMaxPacketSize 2u * 1024u + +String mqttClientId; // Auto-generated MQTT ClientID +/* +String mqttGetSubtopic; // MQTT subtopic for incoming commands requesting .val +String mqttGetSubtopicJSON; // MQTT object buffer for JSON status when requesting .val +String mqttStateTopic; // MQTT topic for outgoing panel interactions +String mqttStateJSONTopic; // MQTT topic for outgoing panel interactions in JSON format +String mqttCommandTopic; // MQTT topic for incoming panel commands +String mqttGroupCommandTopic; // MQTT topic for incoming group panel commands +String mqttStatusTopic; // MQTT topic for publishing device connectivity state +String mqttSensorTopic; // MQTT topic for publishing device information in JSON format +*/ +String mqttLightCommandTopic; // MQTT topic for incoming panel backlight on/off commands +String mqttLightStateTopic; // MQTT topic for outgoing panel backlight on/off state +String mqttLightBrightCommandTopic; // MQTT topic for incoming panel backlight dimmer commands +String mqttLightBrightStateTopic; // MQTT topic for outgoing panel backlight dimmer state +// String mqttMotionStateTopic; // MQTT topic for outgoing motion sensor state + +String mqttNodeTopic; +String mqttGroupTopic; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// These defaults may be overwritten with values saved by the web interface +char mqttServer[64] = MQTT_HOST; +uint16_t mqttPort = MQTT_PORT; +char mqttUser[32] = MQTT_USER; +char mqttPassword[32] = MQTT_PASSW; +// char haspNode[16] = "plate01"; +String mqttGroupName = "plates"; + +/* +const String mqttCommandSubscription = mqttCommandTopic + "/#"; +const String mqttGroupCommandSubscription = mqttGroupCommandTopic + "/#"; +const String mqttLightSubscription = "hasp/" + String(haspGetNodename()) + "/light/#"; +const String mqttLightBrightSubscription = "hasp/" + String(haspGetNodename()) + "/brightness/#"; +*/ + +WiFiClient wifiClient; +PubSubClient mqttClient(wifiClient); + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Send changed values OUT +void mqttSendNewEvent(uint8_t pageid, uint8_t btnid, int32_t val) +{ + char topic[72]; + sprintf_P(topic, PSTR("hasp/%s/state/p[%u].b[%u].event"), haspGetNodename().c_str(), pageid, btnid); + char value[32]; + itoa(val, value, 10); + mqttClient.publish(topic, value); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + String(value)); + + // as json + sprintf_P(topic, PSTR("hasp/%s/state/json"), haspGetNodename().c_str(), pageid, btnid); + sprintf_P(value, PSTR("{\"event\":\"p[%u]].b[%u].event\", \"value\":%u}"), pageid, btnid, val); + mqttClient.publish(topic, value); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + String(value)); +} + +void mqttSendNewValue(uint8_t pageid, uint8_t btnid, int32_t val) +{ + char topic[72]; + sprintf_P(topic, PSTR("hasp/%s/state/p[%u].b[%u].val"), haspGetNodename().c_str(), pageid, btnid); + char value[32]; + itoa(val, value, 10); + mqttClient.publish(topic, value); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + String(value)); + + // as json + sprintf_P(topic, PSTR("hasp/%s/state/json"), haspGetNodename().c_str(), pageid, btnid); + sprintf_P(value, PSTR("{\"event\":\"p[%u]].b[%u].val\", \"value\":%u}"), pageid, btnid, val); + mqttClient.publish(topic, value); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + String(value)); +} + +void mqttSendNewValue(uint8_t pageid, uint8_t btnid, String txt) +{ + char topic[72]; + sprintf_P(topic, PSTR("hasp/%s/state/p[%u].b[%u].txt"), haspGetNodename().c_str(), pageid, btnid); + mqttClient.publish(topic, txt.c_str()); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + txt); + + // as json + char value[64]; + sprintf_P(topic, PSTR("hasp/%s/state/json"), haspGetNodename().c_str(), pageid, btnid); + sprintf_P(value, PSTR("{\"event\":\"p[%u]].b[%u].txt\", \"value\":\"%s\"}"), pageid, btnid, txt.c_str()); + mqttClient.publish(topic, value); + debugPrintln(String(F("MQTT OUT: ")) + String(topic) + " = " + String(value)); +} + +void mqttHandlePage(String strPageid) +{ + if(strPageid.length() == 0) { + String strPayload = String(haspGetPage()); + String topic = mqttNodeTopic + F("state/page"); + char buffer[64]; + sprintf_P(buffer, PSTR("MQTT OUT: %s = %s"), topic.c_str(), strPayload.c_str()); + debugPrintln(buffer); + mqttClient.publish(topic.c_str(), strPayload.c_str()); + } else { + if(strPageid.toInt() <= 250) haspSetPage(strPageid.toInt()); + } +} + +void mqttHandleJson(String & strPayload) +{ // Parse an incoming JSON array into individual Nextion commands + if(strPayload.endsWith( + ",]")) { // Trailing null array elements are an artifact of older Home Assistant automations and need to + // be removed before parsing by ArduinoJSON 6+ + strPayload.remove(strPayload.length() - 2, 2); + strPayload.concat("]"); + } + DynamicJsonDocument nextionCommands(mqttMaxPacketSize + 1024); + DeserializationError jsonError = deserializeJson(nextionCommands, strPayload); + if(jsonError) { // Couldn't parse incoming JSON command + debugPrintln(String(F("MQTT: [ERROR] Failed to parse incoming JSON command with error: ")) + + String(jsonError.c_str())); + return; + } + + for(uint8_t i = 0; i < nextionCommands.size(); i++) { + debugPrintln(nextionCommands[i]); + // nextionSendCmd(nextionCommands[i]); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Receive incoming messages +void mqttCallback(char * topic, byte * payload, unsigned int length) +{ // Handle incoming commands from MQTT + payload[length] = '\0'; + String strTopic = topic; + String strPayload = (char *)payload; + + // strTopic: homeassistant/haswitchplate/devicename/command/p[1].b[4].txt + // strPayload: "Lights On" + // subTopic: p[1].b[4].txt + + // Incoming Namespace (replace /device/ with /group/ for group commands) + // '[...]/device/command' -m '' = No command requested, respond with mqttStatusUpdate() + // '[...]/device/command' -m 'dim=50' = nextionSendCmd("dim=50") + // '[...]/device/command/json' -m '["dim=5", "page 1"]' = nextionSendCmd("dim=50"), nextionSendCmd("page 1") + // '[...]/device/command/page' -m '1' = nextionSendCmd("page 1") + // '[...]/device/command/statusupdate' -m '' = mqttStatusUpdate() + // '[...]/device/command/lcdupdate' -m 'http://192.168.0.10/local/HASwitchPlate.tft' = + // nextionStartOtaDownload("http://192.168.0.10/local/HASwitchPlate.tft") + // '[...]/device/command/lcdupdate' -m '' = nextionStartOtaDownload("lcdFirmwareUrl") + // '[...]/device/command/espupdate' -m 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' = + // espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") + // '[...]/device/command/espupdate' -m '' = espStartOta("espFirmwareUrl") + // '[...]/device/command/p[1].b[4].txt' -m '' = nextionGetAttr("p[1].b[4].txt") + // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' = nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") + + debugPrintln(String(F("MQTT IN: '")) + strTopic + "' : '" + strPayload + "'"); + + if(strTopic.startsWith(mqttNodeTopic)) { + strTopic = strTopic.substring(mqttNodeTopic.length(), strTopic.length()); + } else if(strTopic.startsWith(mqttGroupTopic)) { + strTopic = strTopic.substring(mqttGroupTopic.length(), strTopic.length()); + } else { + return; + } + // debugPrintln(String(F("MQTT Short Topic : '")) + strTopic + "'"); + + if(strTopic == F("command")) { + if(strPayload == "") { // '[...]/device/command' -m '' = No command requested, respond with mqttStatusUpdate() + // mqttStatusUpdate(); // return status JSON via MQTT + } else { // '[...]/device/command' -m 'dim=50' == nextionSendCmd("dim=50") + // nextionSendCmd(strPayload); + } + return; + } + + if(strTopic.startsWith(F("command/"))) { + strTopic = strTopic.substring(8u, strTopic.length()); + // debugPrintln(String(F("MQTT Shorter Command Topic : '")) + strTopic + "'"); + + if(strTopic == F("page")) { // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") + mqttHandlePage(strPayload); + } else if(strTopic == F("dim")) { // '[...]/device/command/page' -m '1' == nextionSendCmd("page 1") +#if defined(ARDUINO_ARCH_ESP32) + ledcWrite(0, map(strPayload.toInt(), 0, 100, 0, 1023)); // ledChannel and value +#else + analogWrite(D1, map(strPayload.toInt(), 0, 100, 0, 1023)); +#endif + + } else if(strTopic == F("json")) { // '[...]/device/command/json' -m '["dim=5", "page 1"]' = + // nextionSendCmd("dim=50"), nextionSendCmd("page 1") + mqttHandleJson(strPayload); // Send to nextionParseJson() + } else if(strTopic == F("statusupdate")) { // '[...]/device/command/statusupdate' == mqttStatusUpdate() + // mqttStatusUpdate(); // return status JSON via MQTT + } else if(strTopic == F("espupdate")) { // '[...]/device/command/espupdate' -m + // 'http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin' == + // espStartOta("http://192.168.0.10/local/HASwitchPlate.ino.d1_mini.bin") + if(strPayload == "") { + // espStartOta(espFirmwareUrl); + } else { + // espStartOta(strPayload); + } + } else if(strTopic == F("reboot")) { // '[...]/device/command/reboot' == reboot microcontroller) + debugPrintln(F("MQTT: Rebooting device")); + haspReset(); + } else if(strTopic == F("lcdreboot")) { // '[...]/device/command/lcdreboot' == reboot LCD panel) + debugPrintln(F("MQTT: Rebooting LCD")); + haspReset(); + } else if(strTopic == F("factoryreset")) { // '[...]/device/command/factoryreset' == clear all saved settings) + // configClearSaved(); + } else if(strPayload == "") { // '[...]/device/command/p[1].b[4].txt' -m '' == nextionGetAttr("p[1].b[4].txt") + haspProcessAttribute(strTopic, ""); + } else { // '[...]/device/command/p[1].b[4].txt' -m '"Lights On"' == + // nextionSetAttr("p[1].b[4].txt", "\"Lights On\"") + haspProcessAttribute(strTopic, strPayload); + } + return; + } + + if(strTopic == mqttLightBrightCommandTopic) { // change the brightness from the light topic + int panelDim = map(strPayload.toInt(), 0, 255, 0, 100); + // nextionSetAttr("dim", String(panelDim)); + // nextionSendCmd("dims=dim"); + // mqttClient.publish(mqttLightBrightStateTopic, strPayload); + } else if(strTopic == mqttLightCommandTopic && + strPayload == F("OFF")) { // set the panel dim OFF from the light topic, saving current dim level first + // nextionSendCmd("dims=dim"); + // nextionSetAttr("dim", "0"); + mqttClient.publish(mqttLightStateTopic.c_str(), PSTR("OFF")); + } else if(strTopic == mqttLightCommandTopic && + strPayload == F("ON")) { // set the panel dim ON from the light topic, restoring saved dim level + // nextionSendCmd("dim=dims"); + mqttClient.publish(mqttLightStateTopic.c_str(), PSTR("ON")); + } + + if(strTopic == F("status") && + strPayload == F("OFF")) { // catch a dangling LWT from a previous connection if it appears + char topicBuffer[64]; + sprintf_P(topicBuffer, PSTR("%sstatus"), mqttNodeTopic.c_str()); + debugPrintln(String(F("MQTT: binary_sensor state: [")) + topicBuffer + "] : ON"); + mqttClient.publish(topicBuffer, "ON", true); + return; + } +} + +void mqttReconnect() +{ + static uint8_t mqttReconnectCount = 0; + bool mqttFirstConnect = true; + String nodeName = haspGetNodename(); + // Generate an MQTT client ID as haspNode + our MAC address + mqttClientId = nodeName + "-" + WiFi.macAddress(); + + char topicBuffer[64]; + sprintf_P(topicBuffer, PSTR("hasp/%s/"), nodeName.c_str()); + mqttNodeTopic = topicBuffer; + sprintf_P(topicBuffer, PSTR("hasp/%s/"), mqttGroupName.c_str()); + mqttGroupTopic = topicBuffer; + + // haspSetPage(0); + debugPrintln(String(F("MQTT: Attempting connection to broker ")) + String(mqttServer) + String(F(" as clientID ")) + + mqttClientId); + + // Attempt to connect and set LWT and Clean Session + sprintf_P(topicBuffer, PSTR("%sstatus"), mqttNodeTopic.c_str()); + if(!mqttClient.connect(mqttClientId.c_str(), mqttUser, mqttPassword, topicBuffer, 0, false, "OFF", true)) { + // Retry until we give up and restart after connectTimeout seconds + mqttReconnectCount++; + + Serial.print(String(F("failed, rc="))); + Serial.print(mqttClient.state()); + // Wait 5 seconds before retrying + // delay(50); + return; + } + + debugPrintln(F("MQTT: [SUCCESS] MQTT Client is Connected")); + haspReconnect(); + + /* + // MQTT topic string definitions + mqttStateTopic = prefix + F("/state"); + mqttStateJSONTopic = prefix + F("/state/json"); + mqttCommandTopic = prefix + F("/page"); + mqttGroupCommandTopic = "hasp/" + mqttGroupName + "/page"; + mqttCommandTopic = prefix + F("/command"); + mqttGroupCommandTopic = "hasp/" + mqttGroupName + "/command"; + mqttSensorTopic = prefix + F("/sensor"); + mqttLightCommandTopic = prefix + F("/light/switch"); + mqttLightStateTopic = prefix + F("/light/state"); + mqttLightBrightCommandTopic = prefix + F("/brightness/set"); + mqttLightBrightStateTopic = prefix + F("/brightness/state"); + mqttMotionStateTopic = prefix + F("/motion/state"); + */ + // Set keepAlive, cleanSession, timeout + // mqttClient.setOptions(30, true, 5000); + + // declare LWT + // mqttClient.setWill(mqttStatusTopic.c_str(), "OFF"); + + // Attempt to connect to broker, setting last will and testament + // Subscribe to our incoming topics + sprintf_P(topicBuffer, PSTR("%scommand/#"), mqttGroupTopic.c_str()); + if(mqttClient.subscribe(topicBuffer)) { + debugPrintln(String(F("MQTT: * Subscribed to ")) + topicBuffer); + } + sprintf_P(topicBuffer, PSTR("%scommand/#"), mqttNodeTopic.c_str()); + if(mqttClient.subscribe(topicBuffer)) { + debugPrintln(String(F("MQTT: * Subscribed to ")) + topicBuffer); + } + sprintf_P(topicBuffer, PSTR("%slight/#"), mqttNodeTopic.c_str()); + if(mqttClient.subscribe(topicBuffer)) { + debugPrintln(String(F("MQTT: * Subscribed to ")) + topicBuffer); + } + sprintf_P(topicBuffer, PSTR("%sbrightness/#"), mqttNodeTopic.c_str()); + if(mqttClient.subscribe(topicBuffer)) { + debugPrintln(String(F("MQTT: * Subscribed to ")) + topicBuffer); + } + + sprintf_P(topicBuffer, PSTR("%sstatus"), mqttNodeTopic.c_str()); + if(mqttClient.subscribe(topicBuffer)) { + debugPrintln(String(F("MQTT: * Subscribed to ")) + topicBuffer); + } + // Force any subscribed clients to toggle OFF/ON when we first connect to + // make sure we get a full panel refresh at power on. Sending OFF, + // "ON" will be sent by the mqttStatusTopic subscription action. + debugPrintln(String(F("MQTT: binary_sensor state: [")) + topicBuffer + "] : " + (mqttFirstConnect ? "OFF" : "ON")); + mqttClient.publish(topicBuffer, mqttFirstConnect ? "OFF" : "ON", true); //, 1); + + mqttFirstConnect = false; + mqttReconnectCount = 0; +} + +void mqttSetup(const JsonObject & settings) +{ + if(!settings[F_CONFIG_HOST].isNull()) { + strcpy(mqttServer, settings[F_CONFIG_HOST]); + } + if(!settings[F_CONFIG_PORT].isNull()) { + mqttPort = settings[F_CONFIG_PORT]; + } + if(!settings[F_CONFIG_USER].isNull()) { + strcpy(mqttUser, settings[F_CONFIG_USER]); + } + if(!settings[F_CONFIG_PASS].isNull()) { + strcpy(mqttPassword, settings[F_CONFIG_PASS]); + } + if(!settings[F_CONFIG_GROUP].isNull()) { + mqttGroupName = settings[F_CONFIG_GROUP].as(); + } + + mqttClient.setServer(mqttServer, 1883); + mqttClient.setCallback(mqttCallback); +} + +void mqttLoop(bool wifiIsConnected) +{ + if(wifiIsConnected && !mqttClient.connected()) + mqttReconnect(); + else + mqttClient.loop(); +} + +bool mqttIsConnected() +{ + return mqttClient.connected(); +} + +void mqttStop() +{ + if(mqttClient.connected()) { + char topicBuffer[64]; + + sprintf_P(topicBuffer, PSTR("%sstatus"), mqttNodeTopic.c_str()); + mqttClient.publish(topicBuffer, "OFF"); + + sprintf_P(topicBuffer, PSTR("%ssensor"), mqttNodeTopic.c_str()); + mqttClient.publish(topicBuffer, "{\"status\": \"unavailable\"}"); + + mqttClient.disconnect(); + debugPrintln(String(F("MQTT: Disconnected from broker"))); + } +} + +bool mqttGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_CONFIG_HOST] == mqttServer && settings[F_CONFIG_PORT] == mqttPort && + settings[F_CONFIG_USER] == mqttUser && settings[F_CONFIG_PASS] == mqttPassword && + settings[F_CONFIG_GROUP] == mqttGroupName) + return false; + + settings[F_CONFIG_GROUP] = mqttGroupName; + settings[F_CONFIG_HOST] = mqttServer; + settings[F_CONFIG_PORT] = mqttPort; + settings[F_CONFIG_USER] = mqttUser; + settings[F_CONFIG_PASS] = mqttPassword; + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/hasp_ota.cpp b/src/hasp_ota.cpp new file mode 100644 index 00000000..a9b2c359 --- /dev/null +++ b/src/hasp_ota.cpp @@ -0,0 +1,64 @@ +#include +#include "ArduinoJson.h" +#include + +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_ota.h" +#include "hasp.h" + +#define F_OTA_URL F("otaurl") + +std::string otaUrl = "http://ota.local"; + +void otaSetup(JsonObject settings) +{ + char buffer[256]; + + if(!settings[F_OTA_URL].isNull()) { + otaUrl = settings[F_OTA_URL].as().c_str(); + sprintf_P(buffer, PSTR("ORA url: %s"), otaUrl.c_str()); + debugPrintln(buffer); + } + + ArduinoOTA.setHostname(String(haspGetNodename()).c_str()); + // ArduinoOTA.setPassword(configPassword); + + ArduinoOTA.onStart([]() { + debugPrintln(F("OTA: update start")); + haspSendCmd("page 0"); + haspSetAttr("p[0].b[1].txt", "\"ESP OTA Update\""); + }); + ArduinoOTA.onEnd([]() { + haspSendCmd("page 0"); + debugPrintln(F("OTA: update complete")); + haspSetAttr("p[0].b[1].txt", "\"ESP OTA Update\\rComplete!\""); + haspReset(); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + haspSetAttr("p[0].b[1].txt", "\"ESP OTA Update\\rProgress: " + String(progress / (total / 100)) + "%\""); + }); + ArduinoOTA.onError([](ota_error_t error) { + debugPrintln(String(F("OTA: ERROR code ")) + String(error)); + if(error == OTA_AUTH_ERROR) + debugPrintln(F("OTA: ERROR - Auth Failed")); + else if(error == OTA_BEGIN_ERROR) + debugPrintln(F("OTA: ERROR - Begin Failed")); + else if(error == OTA_CONNECT_ERROR) + debugPrintln(F("OTA: ERROR - Connect Failed")); + else if(error == OTA_RECEIVE_ERROR) + debugPrintln(F("OTA: ERROR - Receive Failed")); + else if(error == OTA_END_ERROR) + debugPrintln(F("OTA: ERROR - End Failed")); + haspSetAttr("p[0].b[1].txt", "\"ESP OTA FAILED\""); + delay(5000); + // haspSendCmd("page " + String(nextionActivePage)); + }); + ArduinoOTA.begin(); + debugPrintln(F("OTA: Over the Air firmware update ready")); +} + +void otaLoop() +{ + ArduinoOTA.handle(); // Arduino OTA loop +} \ No newline at end of file diff --git a/src/hasp_spiffs.cpp b/src/hasp_spiffs.cpp new file mode 100644 index 00000000..2819bae3 --- /dev/null +++ b/src/hasp_spiffs.cpp @@ -0,0 +1,57 @@ +#include +#include "ArduinoJson.h" + +#include "hasp_conf.h" +#include "hasp_log.h" +#include "hasp_spiffs.h" + +#if LV_USE_HASP_SPIFFS +#if defined(ARDUINO_ARCH_ESP32) +#include "SPIFFS.h" +#endif +#include // Include the SPIFFS library +#endif +/* +void spiffsList() +{ +#if defined(ARDUINO_ARCH_ESP32) + debugPrintln(PSTR("FILE: Listing files on the internal flash:")); + File root = SPIFFS.open("/"); + File file = root.openNextFile(); + while(file) { + char msg[64]; + sprintf(msg, PSTR("FILE: * %s (%u bytes)"), file.name(), (uint32_t)file.size()); + debugPrintln(msg); + file = root.openNextFile(); + } +#endif +#if defined(ARDUINO_ARCH_ESP8266) + debugPrintln(PSTR("FILE: Listing files on the internal flash:")); + Dir dir = SPIFFS.openDir("/"); + while(dir.next()) { + char msg[64]; + sprintf(msg, PSTR("FILE: * %s (%u bytes)"), dir.fileName().c_str(), (uint32_t)dir.fileSize()); + debugPrintln(msg); + } +#endif +} +*/ +void spiffsSetup() +{ + // no spiffs settings + +#if LV_USE_HASP_SPIFFS + char msg[64]; + if(!SPIFFS.begin()) { + sprintf(msg, PSTR("FILE: %sSPI flash init failed. Unable to mount FS.")); + errorPrintln(msg); + } else { + sprintf(msg, PSTR("FILE: [SUCCESS] SPI flash FS mounted")); + debugPrintln(msg); + // spiffsList(); + } +#endif +} + +void spiffsLoop() +{} \ No newline at end of file diff --git a/src/hasp_tft.cpp b/src/hasp_tft.cpp new file mode 100644 index 00000000..14d8e003 --- /dev/null +++ b/src/hasp_tft.cpp @@ -0,0 +1,229 @@ +#include "TFT_eSPI.h" // Graphics and font library for ST7735 driver chip +#include "ArduinoJson.h" + +#ifdef ESP8266 +ADC_MODE(ADC_VCC); // tftShowConfig measures the voltage on the pin +#endif + +#include "hasp_log.h" +#include "hasp_tft.h" + +#define F_TFT_ROTATION F("rotation") +#define F_TFT_FREQUENCY F("frequency") + +int8_t getPinName(int8_t pin); + +void tftSetup(TFT_eSPI & tft, JsonObject settings) +{ + uint8_t rotation = TFT_ROTATION; + if(settings[F_TFT_ROTATION]) rotation = settings[F_TFT_ROTATION]; + uint32_t frequency = SPI_FREQUENCY; + if(settings[F_TFT_FREQUENCY]) frequency = settings[F_TFT_FREQUENCY]; + + char buffer[64]; + sprintf_P(buffer, PSTR("TFT: %d rotation / %d frequency"), rotation, frequency); + debugPrintln(buffer); + + tft.begin(); /* TFT init */ + tft.setRotation(rotation); /* 1/3=Landscape or 0/2=Portrait orientation */ + + tftShowConfig(tft); + /* Load Calibration data */ + +#ifdef ESP8266 + uint16_t calData[5] = {238, 3529, 369, 3532, 6}; +#else + uint16_t calData[5] = {294, 3410, 383, 3491, 4}; +#endif + // uint16_t calData[5] = {0, 0, 0, 0, 0}; + uint8_t calDataOK = 0; + + // Calibrate + if(0) { + tft.fillScreen(TFT_BLACK); + tft.setCursor(20, 0); + // tft.setTextFont(2); + tft.setTextSize(1); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + + tft.println(PSTR("Touch corners as indicated")); + + tft.setTextFont(1); + tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15); + + for(uint8_t i = 0; i < 5; i++) { + Serial.print(calData[i]); + if(i < 4) Serial.print(", "); + } + } + + tft.setTouch(calData); +} + +void tftLoop() +{ + // Nothing to do here +} + +void tftStop() +{} + +void tftOffsetInfo(uint8_t pin, uint8_t x_offset, uint8_t y_offset) +{ + char buffer[64]; + if(x_offset != 0) { + sprintf_P(buffer, PSTR("TFT: R%u x offset = %i"), pin, x_offset); + debugPrintln(buffer); + } + if(y_offset != 0) { + sprintf_P(buffer, PSTR("TFT: R%u y offset = %i"), pin, y_offset); + debugPrintln(buffer); + } +} + +void tftPinInfo(String pinfunction, int8_t pin) +{ + if(pin != -1) { + char buffer[64]; + sprintf_P(buffer, PSTR("TFT: %s = D%i (GPIO %i)"), pinfunction.c_str(), getPinName(pin), pin); + debugPrintln(buffer); + } +} + +void tftCalibrate() +{} + +void tftShowConfig(TFT_eSPI & tft) +{ + setup_t tftSetup; + char buffer[128]; + tft.getSetup(tftSetup); + + sprintf_P(buffer, PSTR("TFT: TFT_eSPI ver = %s"), tftSetup.version.c_str()); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("TFT: Processor = ESP%i"), tftSetup.esp, HEX); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("TFT: Frequency = %i MHz"), ESP.getCpuFreqMHz()); + debugPrintln(buffer); + +#ifdef ESP8266 + sprintf_P(buffer, PSTR("TFT: Voltage = %2.2f V"), ESP.getVcc() / 918.0); + debugPrintln(buffer); // 918 empirically determined +#endif + sprintf_P(buffer, PSTR("TFT: Transactions = %s"), (tftSetup.trans == 1) ? PSTR("Yes") : PSTR("No")); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("TFT: Interface = %s"), (tftSetup.serial == 1) ? PSTR("SPI") : PSTR("Parallel")); + debugPrintln(buffer); +#ifdef ESP8266 + sprintf_P(buffer, PSTR("TFT: SPI overlap = %s"), (tftSetup.overlap == 1) ? PSTR("Yes") : PSTR("No")); + debugPrintln(buffer); +#endif + if(tftSetup.tft_driver != 0xE9D) // For ePaper displays the size is defined in the sketch + { + sprintf_P(buffer, PSTR("TFT: Display driver = %i"), tftSetup.tft_driver); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("TFT: Display width = %i"), tftSetup.tft_width); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("TFT: Display height = %i"), tftSetup.tft_height); + debugPrintln(buffer); + } else if(tftSetup.tft_driver == 0xE9D) + debugPrintln(F("Display driver = ePaper")); + + // Offsets, not all used yet + tftOffsetInfo(0, tftSetup.r0_x_offset, tftSetup.r0_y_offset); + tftOffsetInfo(1, tftSetup.r1_x_offset, tftSetup.r1_y_offset); + tftOffsetInfo(2, tftSetup.r2_x_offset, tftSetup.r2_y_offset); + tftOffsetInfo(3, tftSetup.r3_x_offset, tftSetup.r3_y_offset); + /* replaced by tftOffsetInfo + if(tftSetup.r1_x_offset != 0) Serial.printf("R1 x offset = %i \n", tftSetup.r1_x_offset); + if(tftSetup.r1_y_offset != 0) Serial.printf("R1 y offset = %i \n", tftSetup.r1_y_offset); + if(tftSetup.r2_x_offset != 0) Serial.printf("R2 x offset = %i \n", tftSetup.r2_x_offset); + if(tftSetup.r2_y_offset != 0) Serial.printf("R2 y offset = %i \n", tftSetup.r2_y_offset); + if(tftSetup.r3_x_offset != 0) Serial.printf("R3 x offset = %i \n", tftSetup.r3_x_offset); + if(tftSetup.r3_y_offset != 0) Serial.printf("R3 y offset = %i \n", tftSetup.r3_y_offset); + */ + + tftPinInfo(F("MOSI "), tftSetup.pin_tft_mosi); + tftPinInfo(F("MISO "), tftSetup.pin_tft_miso); + tftPinInfo(F("SCLK "), tftSetup.pin_tft_clk); + +#ifdef ESP8266 + if(tftSetup.overlap == true) { + debugPrintln(F("Overlap selected, following pins MUST be used:\n")); + + debugPrintln(F("MOSI = SD1 (GPIO 8)\n")); + debugPrintln(F("MISO = SD0 (GPIO 7)\n")); + debugPrintln(F("SCK = CLK (GPIO 6)\n")); + debugPrintln(F("TFT_CS = D3 (GPIO 0)\n\n")); + + debugPrintln(F("TFT_DC and TFT_RST pins can be tftSetup defined\n")); + } +#endif + + tftPinInfo(F("TFT_CS "), tftSetup.pin_tft_cs); + tftPinInfo(F("TFT_DC "), tftSetup.pin_tft_dc); + tftPinInfo(F("TFT_RST"), tftSetup.pin_tft_rst); + + tftPinInfo(F("TOUCH_RST"), tftSetup.pin_tch_cs); + + tftPinInfo(F("TFT_WR "), tftSetup.pin_tft_wr); + tftPinInfo(F("TFT_RD "), tftSetup.pin_tft_rd); + + tftPinInfo(F("TFT_D0 "), tftSetup.pin_tft_d0); + tftPinInfo(F("TFT_D1 "), tftSetup.pin_tft_d1); + tftPinInfo(F("TFT_D2 "), tftSetup.pin_tft_d2); + tftPinInfo(F("TFT_D3 "), tftSetup.pin_tft_d3); + tftPinInfo(F("TFT_D4 "), tftSetup.pin_tft_d4); + tftPinInfo(F("TFT_D5 "), tftSetup.pin_tft_d5); + tftPinInfo(F("TFT_D6 "), tftSetup.pin_tft_d6); + tftPinInfo(F("TFT_D7 "), tftSetup.pin_tft_d7); + + uint16_t fonts = tft.fontsLoaded(); + if(fonts & (1 << 1)) debugPrintln(F("Font GLCD loaded\n")); + if(fonts & (1 << 2)) debugPrintln(F("Font 2 loaded\n")); + if(fonts & (1 << 4)) debugPrintln(F("Font 4 loaded\n")); + if(fonts & (1 << 6)) debugPrintln(F("Font 6 loaded\n")); + if(fonts & (1 << 7)) debugPrintln(F("Font 7 loaded\n")); + if(fonts & (1 << 9)) + debugPrintln(F("Font 8N loaded\n")); + else if(fonts & (1 << 8)) + debugPrintln(F("Font 8 loaded\n")); + if(fonts & (1 << 15)) debugPrintln(F("Smooth font enabled\n")); + + if(tftSetup.serial == 1) { + sprintf_P(buffer, PSTR("TFT: Display SPI frequency = %2.1f MHz"), tftSetup.tft_spi_freq / 10.0); + debugPrintln(buffer); + } + if(tftSetup.pin_tch_cs != -1) { + sprintf_P(buffer, PSTR("TFT: Touch SPI frequency = %2.1f MHz"), tftSetup.tch_spi_freq / 10.0); + debugPrintln(buffer); + } +} + +// Get pin name for ESP8266 +int8_t getPinName(int8_t pin) +{ +// For ESP32 pin labels on boards use the GPIO number +#ifdef ESP32 + return pin; +#endif + + // For ESP8266 the pin labels are not the same as the GPIO number + // These are for the NodeMCU pin definitions: + // GPIO Dxx + if(pin == 16) return 0; + if(pin == 5) return 1; + if(pin == 4) return 2; + if(pin == 0) return 3; + if(pin == 2) return 4; + if(pin == 14) return 5; + if(pin == 12) return 6; + if(pin == 13) return 7; + if(pin == 15) return 8; + if(pin == 3) return 9; + if(pin == 1) return 10; + if(pin == 9) return 11; + if(pin == 10) return 12; + + return -1; // Invalid pin +} diff --git a/src/hasp_wifi.cpp b/src/hasp_wifi.cpp new file mode 100644 index 00000000..460afd84 --- /dev/null +++ b/src/hasp_wifi.cpp @@ -0,0 +1,179 @@ +#include +#include "ArduinoJson.h" + +#include "hasp_conf.h" + +#include "hasp_wifi.h" +#include "hasp_mqtt.h" +#include "hasp_http.h" +#include "hasp_log.h" +#include "hasp_debug.h" +#include "hasp_config.h" +#include "hasp_gui.h" +#include "hasp.h" + +#if defined(ARDUINO_ARCH_ESP32) +#include +#else +#include + +static WiFiEventHandler wifiEventHandler[3]; + +#endif + +#include "user_config_override.h" + +std::string wifiSsid = WIFI_SSID; +std::string wifiPassword = WIFI_PASSW; + +// long wifiPrevMillis = 0; +// bool wifiWasConnected = false; +// int8_t wifiReconnectAttempt = -20; + +void wifiConnected(IPAddress ipaddress) +{ + char buffer[64]; + sprintf_P(buffer, PSTR("WIFI: Received IP address %s"), ipaddress.toString().c_str()); + debugPrintln(buffer); + sprintf_P(buffer, PSTR("WIFI: Connected = %s"), WiFi.status() == WL_CONNECTED ? PSTR("yes") : PSTR("no")); + debugPrintln(buffer); + + httpReconnect(); + // mqttReconnect(); + haspReconnect(); +} + +void wifiDisconnected(const char * ssid, uint8_t reason) +{ + char buffer[64]; + sprintf_P(buffer, PSTR("WIFI: Disconnected from %s (Reason: %d)"), ssid, reason); + debugPrintln(buffer); + WiFi.reconnect(); +} + +void wifiSsidConnected(const char * ssid) +{ + char buffer[64]; + sprintf_P(buffer, PSTR("WIFI: Connected to SSID %s. Requesting IP..."), ssid); + debugPrintln(buffer); +} + +#if defined(ARDUINO_ARCH_ESP32) +void wifi_callback(system_event_id_t event, system_event_info_t info) +{ + switch(event) { + case SYSTEM_EVENT_STA_CONNECTED: + wifiSsidConnected((const char *)info.connected.ssid); + break; + case SYSTEM_EVENT_STA_GOT_IP: + wifiConnected(IPAddress(info.got_ip.ip_info.ip.addr)); + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + wifiDisconnected((const char *)info.disconnected.ssid, info.disconnected.reason); + // NTP.stop(); // NTP sync can be disabled to avoid sync errors + break; + default: + break; + } +} +#endif + +#if defined(ARDUINO_ARCH_ESP8266) +void wifiSTAConnected(WiFiEventStationModeConnected info) +{ + wifiSsidConnected(info.ssid.c_str()); +} + +// Start NTP only after IP network is connected +void wifiSTAGotIP(WiFiEventStationModeGotIP info) +{ + wifiConnected(IPAddress(info.ip)); +} + +// Manage network disconnection +void wifiSTADisconnected(WiFiEventStationModeDisconnected info) +{ + wifiDisconnected(info.ssid.c_str(), info.reason); +} +#endif + +void wifiSetup(JsonObject settings) +{ + char buffer[64]; + + if(!settings[F_CONFIG_SSID].isNull()) { + wifiSsid = settings[F_CONFIG_SSID].as().c_str(); + + // sprintf_P(buffer, PSTR("Wifi Ssid: %s"), wifiSsid.c_str()); + // debugPrintln(buffer); + } + if(!settings[F_CONFIG_PASS].isNull()) { + wifiPassword = settings[F_CONFIG_PASS].as().c_str(); + + // sprintf_P(buffer, PSTR("Wifi Password: %s"), wifiPassword.c_str()); + // debugPrintln(buffer); + } + + sprintf_P(buffer, PSTR("WIFI: Connecting to : %s"), wifiSsid.c_str()); + debugPrintln(buffer); + + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSsid.c_str(), wifiPassword.c_str()); + +#if defined(ARDUINO_ARCH_ESP32) + WiFi.onEvent(wifi_callback); +#endif +#if defined(ARDUINO_ARCH_ESP8266) + wifiEventHandler[0] = WiFi.onStationModeGotIP(wifiSTAGotIP); // As soon WiFi is connected, start NTP Client + wifiEventHandler[1] = WiFi.onStationModeDisconnected(wifiSTADisconnected); + wifiEventHandler[2] = WiFi.onStationModeConnected(wifiSTAConnected); +#endif +} + +bool wifiLoop() +{ + return WiFi.status() == WL_CONNECTED; + + /* + if(WiFi.status() == WL_CONNECTED) { + if(wifiWasConnected) return true; + + debugPrintln(F("WIFI: Reconnected")); + wifiWasConnected = true; + wifiReconnectAttempt = 1; + wifiPrevMillis = millis(); + haspOnline(); + return true; + + } else if(millis() - wifiPrevMillis > 1000) { + if(wifiReconnectAttempt < 20) { + if(wifiReconnectAttempt == 1) { // <0 means we were never connected yet + // haspOffline(); + warningPrintln(String(F("WIFI: %sConnection lost. Reconnecting... #")) + + String(wifiReconnectAttempt)); WiFi.reconnect(); } else { debugPrintln(F("WIFI: Waiting for connection...")); + } + } else { + // haspOffline(); + debugPrintln(F("WIFI: Connection lost. Reconnecting...")); + WiFi.reconnect(); + } + wifiReconnectAttempt++; + wifiPrevMillis = millis(); + } + return false;*/ +} + +bool wifiGetConfig(const JsonObject & settings) +{ + if(!settings.isNull() && settings[F_CONFIG_SSID] == String(wifiSsid.c_str()) && + settings[F_CONFIG_PASS] == String(wifiPassword.c_str())) + return false; + + settings[F_CONFIG_SSID] = String(wifiSsid.c_str()); + settings[F_CONFIG_PASS] = String(wifiPassword.c_str()); + + size_t size = serializeJson(settings, Serial); + Serial.println(); + + return true; +} \ No newline at end of file diff --git a/src/lv_theme_hasp.c b/src/lv_theme_hasp.c new file mode 100644 index 00000000..c62de9e0 --- /dev/null +++ b/src/lv_theme_hasp.c @@ -0,0 +1,519 @@ +/** + * @file lv_theme_hasp.c + * + */ + +/********************* + * INCLUDES + *********************/ +#include "../lib/lvgl/src/lv_themes/lv_theme.h" + +#if LV_USE_THEME_HASP + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * STATIC PROTOTYPES + **********************/ + +/********************** + * STATIC VARIABLES + **********************/ +static lv_theme_t theme; +static lv_style_t def; +static lv_style_t scr; + +/*Static style definitions*/ +static lv_style_t sb; +static lv_style_t plain_bordered; +static lv_style_t label_prim; +static lv_style_t label_sec; +static lv_style_t label_hint; + +static lv_style_t btn_rel, btn_pr, btn_trel, btn_tpr, btn_ina; + +/*Saved input parameters*/ +static uint16_t _hue; +static lv_font_t * _font; + +/********************** + * MACROS + **********************/ + +/********************** + * STATIC FUNCTIONS + **********************/ + +static void basic_init(void) +{ + lv_style_plain.text.font = _font; + lv_style_pretty.text.font = _font; + lv_style_pretty_color.text.font = _font; + + lv_style_btn_rel.text.font = _font; + lv_style_btn_pr.text.font = _font; + lv_style_btn_tgl_rel.text.font = _font; + lv_style_btn_tgl_pr.text.font = _font; + lv_style_btn_ina.text.font = _font; + + if(_hue <= 360) { + lv_style_pretty_color.body.main_color = lv_color_hsv_to_rgb(_hue, 10, 70); + lv_style_pretty_color.body.grad_color = lv_color_hsv_to_rgb(_hue, 80, 80); + + lv_style_plain_color.body.main_color = lv_color_hsv_to_rgb(_hue, 50, 75); + lv_style_plain_color.body.grad_color = lv_style_plain_color.body.main_color; + + lv_style_btn_rel.body.main_color = lv_color_hsv_to_rgb(_hue, 10, 70); + lv_style_btn_rel.body.grad_color = lv_color_hsv_to_rgb(_hue, 80, 80); + + lv_style_btn_pr.body.main_color = lv_color_hsv_to_rgb(_hue, 80, 70); + lv_style_btn_pr.body.grad_color = lv_color_hsv_to_rgb(_hue, 10, 80); + + lv_style_btn_tgl_rel.body.main_color = lv_color_hsv_to_rgb(_hue, 10, 70); + lv_style_btn_tgl_rel.body.grad_color = lv_color_hsv_to_rgb(_hue, 80, 80); + + lv_style_btn_tgl_pr.body.main_color = lv_color_hsv_to_rgb(_hue, 80, 70); + lv_style_btn_tgl_pr.body.grad_color = lv_color_hsv_to_rgb(_hue, 10, 80); + } + + lv_style_copy(&def, &lv_style_pretty); /*Initialize the default style*/ + def.text.font = _font; + + lv_style_copy(&scr, &def); + scr.body.padding.bottom = 0; + scr.body.padding.top = 0; + scr.body.padding.left = 0; + scr.body.padding.right = 0; + + lv_style_copy(&sb, &lv_style_pretty_color); + sb.body.grad_color = sb.body.main_color; + sb.body.padding.right = sb.body.padding.right / 2; /*Make closer to the edges*/ + sb.body.padding.bottom = sb.body.padding.bottom / 2; + + lv_style_copy(&plain_bordered, &lv_style_plain); + plain_bordered.body.border.width = 2; + plain_bordered.body.border.color = lv_color_hex3(0xbbb); + + theme.style.bg = &lv_style_plain; + theme.style.scr = &scr; + theme.style.panel = &lv_style_pretty; +} + +static void btn_init(void) +{ +#if LV_USE_BTN != 0 + theme.style.btn.rel = &lv_style_btn_rel; + theme.style.btn.pr = &lv_style_btn_pr; + theme.style.btn.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.btn.tgl_pr = &lv_style_btn_tgl_pr; + theme.style.btn.ina = &lv_style_btn_ina; +#endif +} + +static void label_init(void) +{ +#if LV_USE_LABEL != 0 + + lv_style_copy(&label_prim, &lv_style_plain); + lv_style_copy(&label_sec, &lv_style_plain); + lv_style_copy(&label_hint, &lv_style_plain); + + label_prim.text.color = lv_color_hex3(0x111); + label_sec.text.color = lv_color_hex3(0x888); + label_hint.text.color = lv_color_hex3(0xaaa); + + theme.style.label.prim = &label_prim; + theme.style.label.sec = &label_sec; + theme.style.label.hint = &label_hint; +#endif +} + +static void img_init(void) +{ +#if LV_USE_IMG != 0 + + theme.style.img.light = &def; + theme.style.img.dark = &def; +#endif +} + +static void line_init(void) +{ +#if LV_USE_LINE != 0 + + theme.style.line.decor = &def; +#endif +} + +static void led_init(void) +{ +#if LV_USE_LED != 0 + static lv_style_t led; + + lv_style_copy(&led, &lv_style_pretty_color); + led.body.shadow.width = LV_DPI / 10; + led.body.radius = LV_RADIUS_CIRCLE; + led.body.border.width = LV_DPI / 30; + led.body.border.opa = LV_OPA_30; + led.body.shadow.color = led.body.main_color; + + theme.style.led = &led; +#endif +} + +static void bar_init(void) +{ +#if LV_USE_BAR + + theme.style.bar.bg = &lv_style_pretty; + theme.style.bar.indic = &lv_style_pretty_color; +#endif +} + +static void slider_init(void) +{ +#if LV_USE_SLIDER != 0 + static lv_style_t slider_bg; + lv_style_copy(&slider_bg, &lv_style_pretty); + slider_bg.body.padding.left = LV_DPI / 20; + slider_bg.body.padding.right = LV_DPI / 20; + slider_bg.body.padding.top = LV_DPI / 20; + slider_bg.body.padding.bottom = LV_DPI / 20; + + theme.style.slider.bg = &slider_bg; + theme.style.slider.indic = &lv_style_pretty_color; + theme.style.slider.knob = &lv_style_pretty; +#endif +} + +static void sw_init(void) +{ +#if LV_USE_SW != 0 + static lv_style_t sw_indic, sw_bg; + lv_style_copy(&sw_indic, &lv_style_pretty_color); + sw_indic.body.padding.left = -0; + sw_indic.body.padding.right = -0; + sw_indic.body.padding.top = -0; + sw_indic.body.padding.bottom = -0; + sw_indic.body.padding.inner = -0; + + lv_style_copy(&sw_bg, &lv_style_pretty); + sw_bg.body.padding.left = -0; + sw_bg.body.padding.right = -0; + sw_bg.body.padding.top = -0; + sw_bg.body.padding.bottom = -0; + sw_bg.body.padding.inner = -0; + + theme.style.sw.bg = &sw_bg; + theme.style.sw.indic = &sw_indic; + theme.style.sw.knob_off = &lv_style_pretty; + theme.style.sw.knob_on = &lv_style_pretty; +#endif +} + +static void lmeter_init(void) +{ +#if LV_USE_LMETER != 0 + static lv_style_t lmeter; + lv_style_copy(&lmeter, &lv_style_pretty_color); + lmeter.line.color = lv_color_hex3(0xddd); + lmeter.line.width = 2; + lmeter.body.main_color = lv_color_mix(lmeter.body.main_color, LV_COLOR_WHITE, LV_OPA_50); + lmeter.body.grad_color = lv_color_mix(lmeter.body.grad_color, LV_COLOR_BLACK, LV_OPA_50); + + theme.style.lmeter = &lmeter; +#endif +} + +static void gauge_init(void) +{ +#if LV_USE_GAUGE != 0 + static lv_style_t gauge; + lv_style_copy(&gauge, theme.style.lmeter); + gauge.line.color = theme.style.lmeter->body.grad_color; + gauge.line.width = 2; + gauge.body.main_color = lv_color_hex3(0x888); + gauge.body.grad_color = theme.style.lmeter->body.main_color; + gauge.text.color = lv_color_hex3(0x888); + gauge.text.font = _font; + + theme.style.gauge = &gauge; +#endif +} + +static void chart_init(void) +{ +#if LV_USE_CHART + + theme.style.chart = &lv_style_pretty; +#endif +} + +static void cb_init(void) +{ +#if LV_USE_CB != 0 + + theme.style.cb.bg = &lv_style_transp; + theme.style.cb.box.rel = &lv_style_pretty; + theme.style.cb.box.pr = &lv_style_btn_pr; + theme.style.cb.box.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.cb.box.tgl_pr = &lv_style_btn_tgl_pr; + theme.style.cb.box.ina = &lv_style_btn_ina; +#endif +} + +static void btnm_init(void) +{ +#if LV_USE_BTNM + + theme.style.btnm.bg = &lv_style_pretty; + theme.style.btnm.btn.rel = &lv_style_btn_rel; + theme.style.btnm.btn.pr = &lv_style_btn_pr; + theme.style.btnm.btn.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.btnm.btn.tgl_pr = &lv_style_btn_tgl_pr; + theme.style.btnm.btn.ina = &lv_style_btn_ina; +#endif +} + +static void kb_init(void) +{ +#if LV_USE_KB + + theme.style.kb.bg = &lv_style_pretty; + theme.style.kb.btn.rel = &lv_style_btn_rel; + theme.style.kb.btn.pr = &lv_style_btn_pr; + theme.style.kb.btn.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.kb.btn.tgl_pr = &lv_style_btn_tgl_pr; + theme.style.kb.btn.ina = &lv_style_btn_ina; +#endif +} + +static void mbox_init(void) +{ +#if LV_USE_MBOX + + theme.style.mbox.bg = &lv_style_pretty; + theme.style.mbox.btn.bg = &lv_style_transp; + theme.style.mbox.btn.rel = &lv_style_btn_rel; + theme.style.mbox.btn.pr = &lv_style_btn_tgl_pr; +#endif +} + +static void page_init(void) +{ +#if LV_USE_PAGE + + theme.style.page.bg = &lv_style_pretty; + theme.style.page.scrl = &lv_style_transp_tight; + theme.style.page.sb = &sb; +#endif +} + +static void ta_init(void) +{ +#if LV_USE_TA + + theme.style.ta.area = &lv_style_pretty; + theme.style.ta.oneline = &lv_style_pretty; + theme.style.ta.cursor = NULL; + theme.style.ta.sb = &sb; +#endif +} + +static void list_init(void) +{ +#if LV_USE_LIST != 0 + + theme.style.list.bg = &lv_style_pretty; + theme.style.list.scrl = &lv_style_transp_fit; + theme.style.list.sb = &sb; + theme.style.list.btn.rel = &lv_style_btn_rel; + theme.style.list.btn.pr = &lv_style_btn_pr; + theme.style.list.btn.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.list.btn.tgl_pr = &lv_style_btn_tgl_pr; + theme.style.list.btn.ina = &lv_style_btn_ina; +#endif +} + +static void ddlist_init(void) +{ +#if LV_USE_DDLIST != 0 + + theme.style.ddlist.bg = &lv_style_pretty; + theme.style.ddlist.sel = &lv_style_plain_color; + theme.style.ddlist.sb = &sb; +#endif +} + +static void roller_init(void) +{ +#if LV_USE_ROLLER != 0 + + theme.style.roller.bg = &lv_style_pretty; + theme.style.roller.sel = &lv_style_plain_color; +#endif +} + +static void tabview_init(void) +{ +#if LV_USE_TABVIEW != 0 + + theme.style.tabview.bg = &plain_bordered; + theme.style.tabview.indic = &lv_style_plain_color; + theme.style.tabview.btn.bg = &lv_style_transp; + theme.style.tabview.btn.rel = &lv_style_btn_rel; + theme.style.tabview.btn.pr = &lv_style_btn_pr; + theme.style.tabview.btn.tgl_rel = &lv_style_btn_tgl_rel; + theme.style.tabview.btn.tgl_pr = &lv_style_btn_tgl_pr; +#endif +} + +static void table_init(void) +{ +#if LV_USE_TABLE != 0 + theme.style.table.bg = &lv_style_transp_tight; + theme.style.table.cell = &lv_style_plain; +#endif +} + +static void win_init(void) +{ +#if LV_USE_WIN != 0 + + theme.style.win.bg = &plain_bordered; + theme.style.win.sb = &sb; + theme.style.win.header = &lv_style_plain_color; + theme.style.win.content = &lv_style_transp; + theme.style.win.btn.rel = &lv_style_btn_rel; + theme.style.win.btn.pr = &lv_style_btn_pr; +#endif +} + +#if LV_USE_GROUP + +static void style_mod(lv_group_t * group, lv_style_t * style) +{ + (void)group; /*Unused*/ +#if LV_COLOR_DEPTH != 1 + /*Make the style to be a little bit orange*/ + style->body.border.opa = LV_OPA_COVER; + style->body.border.color = LV_COLOR_ORANGE; + + /*If not empty or has border then emphasis the border*/ + if(style->body.opa != LV_OPA_TRANSP || style->body.border.width != 0) style->body.border.width = LV_DPI / 20; + + style->body.main_color = lv_color_mix(style->body.main_color, LV_COLOR_ORANGE, LV_OPA_70); + style->body.grad_color = lv_color_mix(style->body.grad_color, LV_COLOR_ORANGE, LV_OPA_70); + style->body.shadow.color = lv_color_mix(style->body.shadow.color, LV_COLOR_ORANGE, LV_OPA_60); + + style->text.color = lv_color_mix(style->text.color, LV_COLOR_ORANGE, LV_OPA_70); +#else + style->body.border.opa = LV_OPA_COVER; + style->body.border.color = LV_COLOR_BLACK; + style->body.border.width = 2; +#endif +} + +static void style_mod_edit(lv_group_t * group, lv_style_t * style) +{ + (void)group; /*Unused*/ +#if LV_COLOR_DEPTH != 1 + /*Make the style to be a little bit orange*/ + style->body.border.opa = LV_OPA_COVER; + style->body.border.color = LV_COLOR_GREEN; + + /*If not empty or has border then emphasis the border*/ + if(style->body.opa != LV_OPA_TRANSP || style->body.border.width != 0) style->body.border.width = LV_DPI / 20; + + style->body.main_color = lv_color_mix(style->body.main_color, LV_COLOR_GREEN, LV_OPA_70); + style->body.grad_color = lv_color_mix(style->body.grad_color, LV_COLOR_GREEN, LV_OPA_70); + style->body.shadow.color = lv_color_mix(style->body.shadow.color, LV_COLOR_GREEN, LV_OPA_60); + + style->text.color = lv_color_mix(style->text.color, LV_COLOR_GREEN, LV_OPA_70); +#else + style->body.border.opa = LV_OPA_COVER; + style->body.border.color = LV_COLOR_BLACK; + style->body.border.width = 3; +#endif +} + +#endif /*LV_USE_GROUP*/ + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +/** + * Initialize the default theme + * @param hue [0..360] hue value from HSV color space to define the theme's base color + * @param font pointer to a font (NULL to use the default) + * @return pointer to the initialized theme + */ +lv_theme_t * lv_theme_hasp_init(uint16_t hue, lv_font_t * font) +{ + if(font == NULL) font = LV_FONT_DEFAULT; + + _hue = hue; + _font = font; + + /*For backward compatibility initialize all theme elements with a default style */ + uint16_t i; + lv_style_t ** style_p = (lv_style_t **)&theme.style; + for(i = 0; i < LV_THEME_STYLE_COUNT; i++) { + *style_p = &def; + style_p++; + } + + basic_init(); + btn_init(); + label_init(); + img_init(); + line_init(); + led_init(); + bar_init(); + slider_init(); + sw_init(); + lmeter_init(); + gauge_init(); + chart_init(); + cb_init(); + btnm_init(); + kb_init(); + mbox_init(); + page_init(); + ta_init(); + list_init(); + ddlist_init(); + roller_init(); + tabview_init(); + table_init(); + win_init(); + +#if LV_USE_GROUP + theme.group.style_mod_xcb = style_mod; + theme.group.style_mod_edit_xcb = style_mod_edit; +#endif + + return &theme; +} + +/** + * Get a pointer to the theme + * @return pointer to the theme + */ +lv_theme_t * lv_theme_get_hasp(void) +{ + return &theme; +} + +/********************** + * STATIC FUNCTIONS + **********************/ + +#endif diff --git a/src/lv_theme_hasp.h b/src/lv_theme_hasp.h new file mode 100644 index 00000000..4b27cebc --- /dev/null +++ b/src/lv_theme_hasp.h @@ -0,0 +1,57 @@ +/** + * @file lv_theme_default.h + * + */ + +#ifndef LV_THEME_HASP_H +#define LV_THEME_HASP_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include "lvgl.h" +#include "../lib/lvgl/src/lv_conf_internal.h" + +#if LV_USE_THEME_HASP + +/********************* + * DEFINES + *********************/ + +/********************** + * TYPEDEFS + **********************/ + +/********************** + * GLOBAL PROTOTYPES + **********************/ + +/** + * Initialize the default theme + * @param hue [0..360] hue value from HSV color space to define the theme's base color + * @param font pointer to a font (NULL to use the default) + * @return pointer to the initialized theme + */ +lv_theme_t * lv_theme_hasp_init(uint16_t hue, lv_font_t * font); + +/** + * Get a pointer to the theme + * @return pointer to the theme + */ +lv_theme_t * lv_theme_get_hasp(void); + +/********************** + * MACROS + **********************/ + +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /*LV_THEME_TEMPL_H*/ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..a6e878df --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,134 @@ + +#include "ArduinoJson.h" +#include "TFT_eSPI.h" + +#include "hasp_conf.h" + +#include "hasp_debug.h" +#include "hasp_eeprom.h" +#include "hasp_spiffs.h" +#include "hasp_config.h" +#include "hasp_tft.h" +#include "hasp_gui.h" +//#include "hasp_ota.h" +#include "hasp.h" + +#if LV_USE_HASP_SPIFFS +#if defined(ARDUINO_ARCH_ESP32) +#include "SPIFFS.h" +#endif +#include // Include the SPIFFS library +#endif + +#if LV_USE_HASP_WIFI +#include "hasp_wifi.h" +#endif + +#if LV_USE_HASP_MQTT +#include "hasp_mqtt.h" +#endif + +#if LV_USE_HASP_HTTP +#include "hasp_http.h" +#endif + +bool isConnected; + +void setup() +{ + /* Init Storage */ + eepromSetup(); +#if LV_USE_HASP_SPIFFS + // spiffsSetup(); +#endif + + /* Read Config File */ + DynamicJsonDocument settings(1024); + // configGetConfig(doc); + // JsonObject settings = doc.as(); + configSetup(settings); + + if(!settings[F("pins")][F("TFT_BCKL")].isNull()) { + int8_t pin = settings[F("pins")][F("TFT_BCKL")].as(); +#if defined(ARDUINO_ARCH_ESP32) + if(pin >= 0) + // configure LED PWM functionalitites + ledcSetup(0, 5000, 10); + // attach the channel to the GPIO to be controlled + ledcAttachPin(pin, 0); +#else + pinMode(pin, OUTPUT); +#endif + } + +#if LV_USE_HASP_SDCARD + sdcardSetup(); +#endif + + /* Init Graphics */ + TFT_eSPI screen = TFT_eSPI(); + tftSetup(screen, settings[F("tft")]); + guiSetup(screen, settings[F("gui")]); + + /* Init GUI Application */ + haspSetup(settings[F("hasp")]); + + /* Init Network Services */ +#if LV_USE_HASP_WIFI + wifiSetup(settings[F("wifi")]); + +#if LV_USE_HASP_MQTT + mqttSetup(settings[F("mqtt")]); +#endif + +#if LV_USE_HASP_MDNS + mdnsSetup(settings[F("mdns")]); +#endif + +#if LV_USE_HASP_HTTP + httpSetup(settings[F("http")]); +#endif + + // otaSetup(settings[F("ota")]); +#endif +} + +void loop() +{ + /* Storage Loops */ + // eepromLoop(); + // spiffsLoop(); +#if LV_USE_HASP_SDCARD + // sdcardLoop(); +#endif + + configLoop(); + + /* Graphics Loops */ + // tftLoop(); + guiLoop(); + + /* Application Loops */ + // haspLoop(); + + /* Network Services Loops */ +#if LV_USE_HASP_WIFI > 0 + isConnected = wifiLoop(); + +#if LV_USE_HASP_MQTT > 0 + mqttLoop(isConnected); +#endif + +#if LV_USE_HASP_HTTP > 0 + httpLoop(isConnected); +#endif + +#if LV_USE_HASP_MDNS > 0 + mdnsLoop(wifiIsConnected); +#endif + + // otaLoop(); +#endif + + delay(5); +} \ No newline at end of file diff --git a/src/user_config_override-template.h b/src/user_config_override-template.h new file mode 100644 index 00000000..f084d96e --- /dev/null +++ b/src/user_config_override-template.h @@ -0,0 +1,33 @@ +/*************************************************** + WiFi Settings + **************************************************/ +#define WIFI_SSID "" +#define WIFI_PASSW "" + +/*************************************************** + MQTT Settings + **************************************************/ +#define MQTT_HOST "" +#define MQTT_PORT 1883 +#define MQTT_USER "" +#define MQTT_PASSW "" +#define MQTT_TOPIC "plates" +#define MQTT_CLIENTID "plate01" + +#define MQTT_TELEPERIOD 60000 +#define MQTT_STATEPERIOD 300000 + +/*************************************************** + * Server Settings + **************************************************/ +#define OTA_HOSTNAME "" +#define OTA_SERVER "" +#define OTA_PORT 80 +#define OTA_URL "" + +/*************************************************** + * Syslog Settings + **************************************************/ +#define SYSLOG_SERVER "" +#define SYSLOG_PORT 514 +#define APP_NAME "HASP" \ No newline at end of file