/** * Implementation of AutoConnectAux class. * @file AutoConnectAuxBasisImpl.h * @author hieromon@gmail.com * @version 0.9.7 * @date 2018-11-17 * @copyright MIT license. */ #include "AutoConnect.h" #include "AutoConnectAux.h" #include "AutoConnectElement.h" #include "AutoConnectElementBasisImpl.h" #ifdef AUTOCONNECT_USE_JSON #include "AutoConnectElementJsonImpl.h" #endif /** * Template for auxiliary page composed with AutoConnectAux of user sketch. * The structure of the auxiliary page depends on this template for * the purpose to be incorporated into the AutoConnect Menu. * The page element implemented by AutoConnectElement is placed at the * position of {{AUX_ELEMENT}} token. This token is contained in a *
block with a class defined in 'base-panel' and is held by a *
element with an ID '_aux'. * The JavaScript that named 'sa' at the end of the template determines * the behavior of AutoConnectSubmit. */ const char AutoConnectAux::_PAGE_AUX[] PROGMEM = { "{{HEAD}}" "{{AUX_TITLE}}" "" "" "" "
" "{{MENU_PRE}}" "{{MENU_AUX}}" "{{MENU_POST}}" "
" "" "
    " "{{AUX_ELEMENT}}" "
" "" "
" "
" "" "" "" }; /** * Destructs container of AutoConnectElement and release a unique * pointer of AutoConnect instance. */ AutoConnectAux::~AutoConnectAux() { _addonElm.clear(); _addonElm.swap(_addonElm); if (_ac) _ac.release(); } /** * Add an AutoConnectElement * @param addon A reference of AutoConnectElement. */ void AutoConnectAux::add(AutoConnectElement& addon) { _addonElm.push_back(addon); AC_DBG("%s placed on %s\n", addon.name.length() ? addon.name.c_str() : "*noname", uri()); } /** * Add an AutoConnectElement vector container to the AutoConnectAux page. * @param addons AutoConnectElementVT collection. */ void AutoConnectAux::add(AutoConnectElementVT addons) { for (std::size_t n = 0; n < addons.size(); n++) add(addons[n]); } /** * Releases the AutoConnectElements with the specified name from * the AutoConnectAux page. Releases all AutoConnectElements with * the same name in AutoConnectAux. * @param name * @return true The specified AutoConnectElements have been released. * @return false The specified AutoConnectElement not found in AutoConnectAux. */ bool AutoConnectAux::release(const String name) { bool rc = false; for (std::size_t n = 0; n < _addonElm.size(); n++) { String elmName = _addonElm[n].get().name; if (elmName == name) { AC_DBG("%s release from %s\n", elmName.c_str(), uri()); _addonElm.erase(_addonElm.begin() + n); rc = true; } } return rc; } /** * Concatenates subsequent AutoConnectAux pages starting from oneself * to the chain list. * AutoConnectAux is collected in the chain list and each object is * chained by the "_next". AutoConnect follows the "_next" to manage * auxiliary pages. The _concat function concatenates subsequent * AutoConnectAuxs. * @param aux A reference of AutoConnectAux. */ void AutoConnectAux::_concat(AutoConnectAux& aux) { if (_next) _next->_concat(aux); else _next.reset(&aux); } /** * Register the AutoConnect that owns itself. * AutoConenctAux needs to access the AutoConnect member. Also * AutoConnectAux is cataloged by chain list. The _join function * registers AutoConnect in the following AutoConnectAux chain list. * @param ac A reference of AutoConnect. */ void AutoConnectAux::_join(AutoConnect& ac) { _ac.reset(&ac); // Chain to subsequent AutoConnectAux in the list. if (_next) _next->_join(ac); } /** * Insert the token handler of PageBuilder. This handler inserts HTML * elements generated by the whole AutoConnectElements to the auxiliary page. * @param args A reference of PageArgument but unused. * @return HTML string that should be inserted. */ const String AutoConnectAux::_insertElement(PageArgument& args) { AC_UNUSED(args); String body = String(); if (_handler) { if (_order & AC_EXIT_AHEAD) { AC_DBG("CB %s\n", uri()); body += _handler(args); } } for (std::size_t n = 0; n < _addonElm.size(); n++) { AutoConnectElement& addon = _addonElm[n]; body += addon.toHTML(); } if (_handler) { if (_order & AC_EXIT_LATER) { AC_DBG("CB %s\n", uri()); body += _handler(args); } } return body; } /** * Generate an auxiliary page assembled with the AutoConnectElement. * This function is the core procedure of AutoConnectAux, and uses * PageBuilder from the _PAGE_AUX template to build an AutoConnect * menu and insert HTML elements. A template of an auxiliary page is * fixed and its structure inherits from the AutoConnect. * @param uri An uri of the auxiliary page. * @return A PageElement of auxiliary page. */ PageElement* AutoConnectAux::_setupPage(String uri) { PageElement* elm = nullptr; if (_ac) { if (uri != String(_uri)) { if (_next) { elm = _next->_setupPage(uri); } } else { AutoConnect* mother = _ac.get(); // Overwrite actual AutoConnectMenu title to the Aux. page title if (_title.length()) mother->_menuTitle = _title; elm = new PageElement(); // Construct the auxiliary page elm->setMold(_PAGE_AUX); elm->addToken(PSTR("HEAD"), std::bind(&AutoConnect::_token_HEAD, mother, std::placeholders::_1)); elm->addToken(PSTR("AUX_TITLE"), std::bind(&AutoConnectAux::_injectTitle, this, std::placeholders::_1)); elm->addToken(PSTR("CSS_BASE"), std::bind(&AutoConnect::_token_CSS_BASE, mother, std::placeholders::_1)); elm->addToken(PSTR("CSS_UL"), std::bind(&AutoConnect::_token_CSS_UL, mother, std::placeholders::_1)); elm->addToken(PSTR("CSS_INPUT_BUTTON"), std::bind(&AutoConnect::_token_CSS_INPUT_BUTTON, mother, std::placeholders::_1)); elm->addToken(PSTR("CSS_INPUT_TEXT"), std::bind(&AutoConnect::_token_CSS_INPUT_TEXT, mother, std::placeholders::_1)); elm->addToken(PSTR("CSS_LUXBAR"), std::bind(&AutoConnect::_token_CSS_LUXBAR, mother, std::placeholders::_1)); elm->addToken(PSTR("MENU_PRE"), std::bind(&AutoConnect::_token_MENU_PRE, mother, std::placeholders::_1)); elm->addToken(PSTR("MENU_AUX"), std::bind(&AutoConnect::_token_MENU_AUX, mother, std::placeholders::_1)); elm->addToken(PSTR("MENU_POST"), std::bind(&AutoConnect::_token_MENU_POST, mother, std::placeholders::_1)); elm->addToken(PSTR("AUX_ELEMENT"), std::bind(&AutoConnectAux::_insertElement, this, std::placeholders::_1)); } } return elm; } /** * Inject the
  • element depending on the "luxbar-item" attribute * for implementing the AutoConnect menu. * @param args A reference of PageArgument but it's only used for * interface alignment and is not actually used. * @return A concatenated string of
  • elements for the menu item of * AutoConnect. */ const String AutoConnectAux::_injectMenu(PageArgument& args) { String menuItem; if (_menu) menuItem = String(FPSTR("
  • ") + _title + String(FPSTR("
  • ")); if (_next) menuItem += _next->_injectMenu(args); return menuItem; } #ifdef AUTOCONNECT_USE_JSON /** * Static storage for JSON buffer size calculation. */ int16_t AutoConnectAux::_jbSize; /**< JSON dynamic buffer size */ uint16_t AutoConnectAux::_jbByte; /**< Byte count for calculation of JSON buffer */ uint8_t AutoConnectAux::_jbObject; /**< Object count for calculation of JSON buffer */ uint8_t AutoConnectAux::_jbArray; /**< Array count for calculation of JSON buffer */ uint8_t AutoConnectAux::_jbNest; /**< JSON array nest count */ uint8_t AutoConnectAux::_kStack[AUTOCONENCT_JSONOBJECTTREE_MAXDEPTH]; /**< JSON array counter stack */ uint8_t AutoConnectAux::_nStack[AUTOCONENCT_JSONOBJECTTREE_MAXDEPTH]; /**< JSON object counter stack */ int8_t AutoConnectAux::_kp; /**< Stack pointer for JSON array counter */ int8_t AutoConnectAux::_np; /**< Stack pointer for JSON object counter */ bool AutoConnectAux::_jbOpen; /**< JSON object paring status */ bool AutoConnectAux::_jbLiteral; /**< JSON object lexical status */ /** * Load AutoConnectAux page from JSON description stored in the sketch. * This function can load AutoConnectAux for multiple AUX pages written * in JSON and is registered in AutoConnect. * @param aux JSON description to be load. * @return true Successfully loaded. */ bool AutoConnect::join(const char* aux) { const size_t bufferSize = AutoConnectAux::_calcJsonBufferSize(aux); DynamicJsonBuffer jsonBuffer(bufferSize); JsonVariant jv = jsonBuffer.parse(aux); return _load(jv); } /** * Load AutoConnectAux page from JSON description stored in PROGMEM. * This function can load AutoConnectAux for multiple AUX pages written * in JSON and is registered in AutoConnect. * @param aux JSON description to be load. * @return true Successfully loaded. */ bool AutoConnect::join(const __FlashStringHelper* aux) { const size_t bufferSize = AutoConnectAux::_calcJsonBufferSize(aux); DynamicJsonBuffer jsonBuffer(bufferSize); JsonVariant jv = jsonBuffer.parse(aux); return _load(jv); } /** * Load AutoConnectAux page from JSON description from the stream. * This function can load AutoConnectAux for multiple AUX pages written * in JSON and is registered in AutoConnect. * @param aux Stream for read AutoConnectAux elements. * @return true Successfully loaded. */ bool AutoConnect::join(Stream& aux, size_t bufferSize) { DynamicJsonBuffer jsonBuffer(bufferSize); JsonVariant jv = jsonBuffer.parse(aux); return _load(jv); } /** * Load AutoConnectAux page from JSON object. * @param aux A JsonVariant object that stores each element of AutoConnectAuxl. * @return true Successfully loaded. */ bool AutoConnect::_load(JsonVariant& aux) { bool rc = true; if (aux.success()) { if (aux.is()) { JsonArray& jb = aux.as(); for (JsonObject& auxJson : jb) { AutoConnectAux* newAux = new AutoConnectAux; if (newAux->_load(auxJson)) join(*newAux); else { delete newAux; rc = false; break; } } } else { JsonObject& jb = aux.as(); AutoConnectAux* newAux = new AutoConnectAux; if (newAux->_load(jb)) join(*newAux); else { delete newAux; rc = false; } } } else return rc; } /** * Create an instance from the AutoConnectElement of the JSON object. * @param json A reference of JSON * @return A pointer of created AutoConnectElement instance. */ AutoConnectElement* AutoConnectAux::_createElement(const JsonObject& json) { AutoConnectElement* elm = nullptr; String type = json.get(F(AUTOCONNECT_JSON_KEY_TYPE)); switch (_asElementType(type)) { case AC_Element: elm = new AutoConnectElement; break; case AC_Button: { AutoConnectButton* cert_elm = new AutoConnectButton; return reinterpret_cast(cert_elm); } case AC_Checkbox: { AutoConnectCheckbox* cert_elm = new AutoConnectCheckbox; return reinterpret_cast(cert_elm); } case AC_Input: { AutoConnectInput* cert_elm = new AutoConnectInput; return reinterpret_cast(cert_elm); } case AC_Radio: { AutoConnectRadio* cert_elm = new AutoConnectRadio; return reinterpret_cast(cert_elm); } case AC_Select: { AutoConnectSelect* cert_elm = new AutoConnectSelect; return reinterpret_cast(cert_elm); } case AC_Submit: { AutoConnectSubmit* cert_elm = new AutoConnectSubmit; return reinterpret_cast(cert_elm); } case AC_Text: { AutoConnectText* cert_elm = new AutoConnectText; return reinterpret_cast(cert_elm); } } return elm; } /** * Constructs an AutoConnectAux instance by reading all the * AutoConnectElements of the specified URI from the elements defined * JSON stored in a constant character string. * @param in AutoConnectAux element data which is described by JSON. * @return true The element collection successfully loaded. * @return false Invalid JSON data occurred. */ bool AutoConnectAux::load(const char* in) { const size_t bufferSize = _calcJsonBufferSize(in); DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _load(jb); } /** * Constructs an AutoConnectAux instance by reading all the * AutoConnectElements of the specified URI from the elements defined * JSON stored in pgm_data array. * @param in AutoConnectAux element data which is described by JSON. * @return true The element collection successfully loaded. * @return false Invalid JSON data occurred. */ bool AutoConnectAux::load(const __FlashStringHelper* in) { const size_t bufferSize = _calcJsonBufferSize(in); DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _load(jb); } /** * Constructs an AutoConnectAux instance by reading all the * AutoConnectElements of the specified URI from the elements defined * JSON stored in a Stream. * @param in AutoConnectAux element data which is described by JSON. * @return true The element collection successfully loaded. * @return false Invalid JSON data occurred. */ bool AutoConnectAux::load(Stream& in, const size_t bufferSize) { DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _load(jb); } /** * Load all elements of AutoConectAux page from JSON object. * @param jb Reference of JSON object * @return true Successfully loaded. * @return false loading unsuccessful, JSON parsing error occurred. */ bool AutoConnectAux::_load(JsonObject& jb) { if (!jb.success()) { AC_DBG("json parse error\n"); return false; } _title = jb.get(F(AUTOCONNECT_JSON_KEY_TITLE)); _uriStr = jb.get(F(AUTOCONNECT_JSON_KEY_URI)); _uri = _uriStr.c_str(); _menu = jb.get(F(AUTOCONNECT_JSON_KEY_MENU)); (void)_loadElement(jb, "*"); return true; } /** * Load element specified by the name parameter from the stream * described by JSON. Usually, the Stream is specified a storm file of * SD or SPIFFS. The Stream must be opened before invoking the function. * @param in Reference of the Stream which contains the parameter * data described by JSON. * @param name The element name to be loaded. '*'specifies that all * elements are to be loaded. * @return A reference of loaded AutoConnectElement instance. */ AutoConnectElement& AutoConnectAux::loadElement(const char* in, const String name) { const size_t bufferSize = _calcJsonBufferSize(in); DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _loadElement(jb, name); } AutoConnectElement& AutoConnectAux::loadElement(const __FlashStringHelper* in, const String name) { const size_t bufferSize = _calcJsonBufferSize(in); DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _loadElement(jb, name); } AutoConnectElement& AutoConnectAux::loadElement(Stream& in, const String name, const size_t bufferSize) { DynamicJsonBuffer jsonBuffer(bufferSize); JsonObject& jb = jsonBuffer.parseObject(in); return _loadElement(jb, name); } AutoConnectElement& AutoConnectAux::_loadElement(JsonObject& jb, const String name) { AutoConnectElement* auxElm = nullptr; bool wc = name == "*"; if (!jb.success()) { AC_DBG("json parse error\n"); return _nullElement(); } JsonArray& elements = jb[AUTOCONNECT_JSON_KEY_ELEMENT]; for (JsonObject& element : elements) { String elmName = element.get(F(AUTOCONNECT_JSON_KEY_NAME)); if (wc || name.equalsIgnoreCase(elmName)) { // The specified element is defined in the JSON stream. // Loads from JSON object. auxElm = _getElement(elmName); // The element is not created yet, create new one. if (!auxElm) { if ((auxElm = _createElement(element))) { AC_DBG("%s<%d> of %s created\n", elmName.c_str(), (int)(auxElm->typeOf()), uri()); add(*auxElm); // Insert to AutoConnect } else { AC_DBG("%s unknown element type\n", elmName.c_str()); continue; } } if (auxElm->loadElement(element)) AC_DBG("%s<%d> of %s loaded\n", auxElm->name.c_str(), (int)auxElm->typeOf(), uri()); else { // Element type mismatch AC_DBG("Type of %s element mismatched\n", elmName.c_str()); continue; } } } return auxElm ? *auxElm : _nullElement(); } /** * Serialize a element to JSON and write it to the stream. * @param out An output stream * @param element A reference of the element to be output. * @return Number of byte output */ size_t AutoConnectAux::saveElement(Stream& out, const AutoConnectElement& element) { DynamicJsonBuffer jsonBuffer; JsonObject& jb = jsonBuffer.parseObject(out); if (!jb.success()) return 0; JsonArray& aux = jb["aux"]; if (!aux.success()) return 0; for (JsonObject& page : aux) { if (page["aux"].as() == String(uri())) { JsonArray& element_j = page[AUTOCONNECT_JSON_KEY_ELEMENT]; for (JsonObject& elm : element_j) { if (elm[AUTOCONNECT_JSON_KEY_NAME].as() == element.name) { elm.set(F(AUTOCONNECT_JSON_KEY_VALUE), element.value); return jb.prettyPrintTo(out); } } } } return 0; } /** * Get already registered AutoConnectElement. * @param name Element name * @return A pointer to the registered AutoConnectElement. */ AutoConnectElement* AutoConnectAux::_getElement(const String name) { for (std::size_t n = 0; n < _addonElm.size(); n++) if (_addonElm[n].get().name == name) return &(_addonElm[n].get()); return nullptr; } /** * Convert element type from type as String. * @param type An element type as String * @return A type of ACElement_t */ const ACElement_t AutoConnectAux::_asElementType(const String type) { typedef struct { const char* tName; ACElement_t tEnum; } ACElementType_t; static const ACElementType_t types[] PROGMEM = { { AUTOCONNECT_JSON_TYPE_ACBUTTON, AC_Button }, { AUTOCONNECT_JSON_TYPE_ACCHECKBOX, AC_Checkbox }, { AUTOCONNECT_JSON_TYPE_ACELEMENT, AC_Element }, { AUTOCONNECT_JSON_TYPE_ACINPUT, AC_Input }, { AUTOCONNECT_JSON_TYPE_ACRADIO, AC_Radio }, { AUTOCONNECT_JSON_TYPE_ACSELECT, AC_Select }, { AUTOCONNECT_JSON_TYPE_ACSUBMIT, AC_Submit }, { AUTOCONNECT_JSON_TYPE_ACTEXT, AC_Text } }; ACElement_t t = AC_Unknown; for (size_t n = 0; n < (sizeof(types) / sizeof(ACElementType_t)); n++) { if (type.equalsIgnoreCase(String(types[n].tName))) return types[n].tEnum; } return t; } /** * Returns a null element as static storage. * This static element is referred by invalid JSON data. * @return A reference of a static element defined by name as null. */ AutoConnectElement& AutoConnectAux::_nullElement() { static AutoConnectElement nullElement("", ""); return nullElement; } /** * Calculate JSON dynamic buffer size. * @param in JSON string * @return Estimated buffer size. */ size_t AutoConnectAux::_calcJsonBufferSize(const char* in) { _initJsonBufferSize(); while (*in) _accJsonBufferSize(*in++); return _resultJsonBufferSize(); } /** * Calculate JSON dynamic buffer size. * @param in JSON string stored in pgm_data. * @return Estimated buffer size. */ size_t AutoConnectAux::_calcJsonBufferSize(const __FlashStringHelper* in) { _initJsonBufferSize(); uint8_t c = pgm_read_byte_near(reinterpret_cast(in)); size_t l = 0; while (c) { _accJsonBufferSize(static_cast(c)); c = pgm_read_byte_near(reinterpret_cast(in) + ++l); } return _resultJsonBufferSize(); } /** * Initialize the stacks for JSON Dynamic buffer size calculation. */ void AutoConnectAux::_initJsonBufferSize() { _jbSize = 0; _jbByte = 0; _jbObject = 0; _jbArray = 0; _jbNest = 0; _kp = -1; _np = -1; _jbOpen = false; _jbLiteral = false; } /** * Accumulate JSON Dynamic buffer size. */ void AutoConnectAux::_accJsonBufferSize(const char c) { if (_jbSize < 0) return; if (!isGraph(c)) return; if (_jbLiteral) _jbByte++; switch (c) { case '"': _jbLiteral = !_jbLiteral; break; case ':': _jbObject++; _jbOpen = false; break; case '{': if (_jbObject > 0) { if (_np >= AUTOCONENCT_JSONOBJECTTREE_MAXDEPTH) { _jbSize = -1; break; } _nStack[++_np] = _jbObject; } _jbObject = 0; _jbOpen = true; break; case '}': if (_jbNest > 0) _jbArray++; if (_jbObject > 0) { _jbSize += JSON_OBJECT_SIZE(_jbObject); _jbByte += 2; } _jbObject = _nStack[_np--]; _jbOpen = false; break; case '[': if (_jbNest++ > 0) { if (_kp >= AUTOCONENCT_JSONOBJECTTREE_MAXDEPTH) { _jbSize = -1; break; } _kStack[++_kp] = _jbArray; } _jbArray = 0; if (_jbObject > 0) { if (_np >= AUTOCONENCT_JSONOBJECTTREE_MAXDEPTH) { _jbSize = -1; break; } _nStack[++_np] = _jbObject; _jbObject = 0; } _jbOpen = true; break; case ']': if (_jbOpen) _jbArray++; _jbSize += JSON_ARRAY_SIZE(_jbArray); _jbByte += 2; _jbArray = _nStack[_kp--]; _jbNest--; if (_np >= 0) _jbObject = _nStack[_np--]; _jbObject = false; break; case ',': if (_jbObject && _jbNest > 0) _jbArray++; break; } } /** * Retrieve accumulated result value of JSON dynamic buffer size. * @return the JSON Dynamic Buffer Size */ size_t AutoConnectAux::_resultJsonBufferSize() { if (_jbSize < 0) { AC_DBG("json buffer calculation error\n"); return -1; } else { AC_DBG("json buffer size:%d\n", _jbSize + _jbByte + 200); return static_cast(_jbSize + _jbByte + 200); } } #endif