diff --git a/.travis.yml b/.travis.yml index 816840b..4b32a7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: - if [[ "$BOARD" =~ "esp32:esp32:" ]]; then arduino --install-boards esp32:esp32; fi - - arduino --install-library PubSubClient,ArduinoJson:5.13.4,PageBuilder:1.3.2 + - arduino --install-library PubSubClient,ArduinoJson:5.13.4,PageBuilder:1.3.3 - buildExampleSketch() { arduino --verify --board $BOARD $PWD/examples/$1/$1.ino; } install: - mkdir -p ~/Arduino/libraries @@ -39,3 +39,4 @@ script: - buildExampleSketch mqttRSSI - buildExampleSketch mqttRSSI_FS - buildExampleSketch mqttRSSI_NA + - buildExampleSketch FileUpload diff --git a/examples/FileUpload/FileUpload.ino b/examples/FileUpload/FileUpload.ino new file mode 100644 index 0000000..53f3a6f --- /dev/null +++ b/examples/FileUpload/FileUpload.ino @@ -0,0 +1,148 @@ +/* + FileUpload.ino, Example for the AutoConnect library. + Copyright (c) 2019, Hieromon Ikasamo + https://github.com/Hieromon/AutoConnect + + This software is released under the MIT License. + https://opensource.org/licenses/MIT + + FileUpload.ino writes the file uploaded from the HTTP client to SPIFFS + with its file name. To run this example successfully, you need the + SPIFFS area setting with Arduino IDE Tool menu which is larger than + the size of the upload file. + + This example leverages the AutoConnectFile element to upload files to + the flash on ​the ESP8266/ESP32 module. The necessary basic process + for uploading and storing to flash is already embedded in the + AutoConnectFile element. By the sketch, just place the AutoConnectFile + element on a custom web page. +*/ + +#if defined(ARDUINO_ARCH_ESP8266) +#include +#include +#elif defined(ARDUINO_ARCH_ESP32) +#include +#include +#include +#endif +#include +#include + +static const char PAGE_UPLOAD[] PROGMEM = R"( +{ + "uri": "/", + "title": "Upload", + "menu": true, + "element": [ + { + "name": "caption", + "type": "ACText", + "value": "

File uploading platform

" + }, + { + "name": "filename", + "type": "ACFile", + "label": "Select file: ", + "store": "fs" + }, + { + "name": "upload", + "type": "ACSubmit", + "value": "UPLOAD", + "uri": "/upload" + } + ] +} +)"; + +static const char PAGE_BROWSE[] PROGMEM = R"( +{ + "uri": "/upload", + "title": "Upload", + "menu": false, + "element": [ + { + "name": "caption", + "type": "ACText", + "value": "

Uploading ended

" + }, + { + "name": "filename", + "type": "ACText" + }, + { + "name": "size", + "type": "ACText" + }, + { + "name": "content_type", + "type": "ACText", + "value": "Content: " + } + ] +} +)"; + +#if defined(ARDUINO_ARCH_ESP8266) +typedef ESP8266WebServer WiFiWebServer; +#elif defined(ARDUINO_ARCH_ESP32) +typedef WebServer WiFiWebServer; +#endif + +WiFiWebServer server; +AutoConnect portal(server); +AutoConnectAux auxUpload; +AutoConnectAux auxBrowse; + +/** + * Post uploading, AutoConnectFile's built-in upload handler reads the + * file saved in SPIFFS and displays the file contents on /upload custom + * web page. However, only files with mime type uploaded as text are + * displayed. A custom web page handler is called after upload. + * @param aux AutoConnectAux(/upload) + * @param args PageArgument + * @return Uploaded text content + */ +String postUpload(AutoConnectAux& aux, PageArgument& args) { + String content; + String filename = auxUpload.getElement("filename").value; + aux.getElement("filename").value = filename; + aux.getElement("size").value = String(auxUpload.getElement("filename").size) + String("bytes uploaded"); + String contentType = auxUpload.getElement("filename").mimeType; + aux.getElement("content_type").value = contentType; + if (contentType.indexOf("text/") >= 0) { + SPIFFS.begin(); + File uploadFile = SPIFFS.open(String("/" + filename).c_str(), "r"); + if (uploadFile) { + while (uploadFile.available()) { + char c = uploadFile.read(); + if (c == '\n') + content += "
"; + else + content += c; + } + uploadFile.close(); + } + else + content = "Not saved"; + SPIFFS.end(); + } + return content; +} + +void setup() { + delay(1000); + Serial.begin(115200); + Serial.println(); + + auxUpload.load(PAGE_UPLOAD); + auxBrowse.load(PAGE_BROWSE); + portal.join({ auxUpload, auxBrowse }); + auxBrowse.on(postUpload); + portal.begin(); +} + +void loop() { + portal.handleClient(); +} diff --git a/keywords.txt b/keywords.txt index f8103f9..e54c76e 100644 --- a/keywords.txt +++ b/keywords.txt @@ -8,6 +8,7 @@ AutoConnectAux KEYWORD1 AutoConnectButton KEYWORD1 AutoConnectCheckbox KEYWORD1 AutoConnectElement KEYWORD1 +AutoConnectFile KEYWORD1 AutoConnectInput KEYWORD1 AutoConnectRadio KEYWORD1 AutoConnectSelect KEYWORD1 @@ -39,6 +40,7 @@ name KEYWORD2 on KEYWORD2 onDetect KEYWORD2 onNotFound KEYWORD2 +onUpload KEYWORD2 release KEYWORD2 save KEYWORD2 saveElement KEYWORD2 @@ -64,6 +66,7 @@ AC_EXIT_BOTH LITERAL1 AC_Button LITERAL1 AC_Checkbox LITERAL1 AC_Element LITERAL1 +AC_File LITERAL1 AC_Input LITERAL1 AC_Radio LITERAL1 AC_Select LITERAL1 @@ -76,6 +79,7 @@ AC_Text LITERAL1 ACButton PREPROCESSOR ACCheckbox PREPROCESSOR ACElement PREPROCESSOR +ACFile PREPROCESSOR ACInput PREPROCESSOR ACRadio PREPROCESSOR ACSelect PREPROCESSOR diff --git a/library.json b/library.json index b2612f3..0ac83ed 100644 --- a/library.json +++ b/library.json @@ -12,7 +12,7 @@ [ { "name": "PageBuilder", - "version": ">=1.3.2" + "version": ">=1.3.3" } ], "frameworks": "arduino", @@ -21,6 +21,6 @@ "espressif8266", "espressif32" ], - "version": "0.9.7", + "version": "0.9.8", "license": "MIT" } diff --git a/library.properties b/library.properties index 21b8108..168d52a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=AutoConnect -version=0.9.7 +version=0.9.8 author=Hieromon Ikasamo maintainer=Hieromon Ikasamo sentence=ESP8266/ESP32 WLAN configuration at runtime with web interface. diff --git a/mkdocs/index.md b/mkdocs/index.md index 4fcfefa..257bf62 100644 --- a/mkdocs/index.md +++ b/mkdocs/index.md @@ -86,7 +86,7 @@ Install third-party platform using the *Boards Manager* of Arduino IDE. You can The [PageBuilder](https://github.com/Hieromon/PageBuilder) library to build HTML for ESP8266WebServer is needed. To install the PageBuilder library into your Arduino IDE, you can use the *Library Manager*. Select the board of ESP8266 series in the Arduino IDE, open the library manager and search keyword '**PageBuilder**' with the topic '**Communication**', then you can see the *PageBuilder*. The latest version is required **1.3.2** **later**.[^1] -[^1]:Since AutoConnect v0.9.7, PageBuilder v1.3.2 later is required. +[^1]:Since AutoConnect v0.9.8, PageBuilder v1.3.3 later is required. diff --git a/src/AutoConnect.cpp b/src/AutoConnect.cpp index b30fbf3..f5b55f5 100644 --- a/src/AutoConnect.cpp +++ b/src/AutoConnect.cpp @@ -392,6 +392,7 @@ void AutoConnect::_startWebServer(void) { _responsePage->chunked(AUTOCONNECT_HTTP_TRANSFER); _responsePage->reserve(AUTOCONNECT_CONTENTBUFFER_SIZE); _responsePage->exitCanHandle(std::bind(&AutoConnect::_classifyHandle, this, std::placeholders::_1, std::placeholders::_2)); + _responsePage->onUpload(std::bind(&AutoConnect::_handleUpload, this, std::placeholders::_1, std::placeholders::_2)); _responsePage->insert(*_webServer); _webServer->begin(); @@ -598,7 +599,7 @@ void AutoConnect::_stopPortal(void) { bool AutoConnect::_captivePortal(void) { String hostHeader = _webServer->hostHeader(); if (!_isIP(hostHeader) && (hostHeader != WiFi.localIP().toString()) && (!hostHeader.endsWith(F(".local")))) { - AC_DBG("Detected appliaction, %s, %s\n", hostHeader.c_str(), WiFi.localIP().toString().c_str()); + AC_DBG("Detected application, %s, %s\n", hostHeader.c_str(), WiFi.localIP().toString().c_str()); String location = String(F("http://")) + _webServer->client().localIP().toString() + String(AUTOCONNECT_URI); _webServer->sendHeader(String(F("Location")), location, true); _webServer->send(302, String(F("text/plain")), _emptyString); @@ -788,7 +789,7 @@ String AutoConnect::_invokeResult(PageArgument& args) { bool AutoConnect::_classifyHandle(HTTPMethod method, String uri) { AC_UNUSED(method); _portalAccessPeriod = millis(); - AC_DBG("Host:%s, URI:%s", _webServer->hostHeader().c_str(), uri.c_str()); + AC_DBG("Host:%s,URI:%s", _webServer->hostHeader().c_str(), uri.c_str()); // When handleClient calls RequestHandler, the parsed http argument // remains the previous request. @@ -801,6 +802,8 @@ bool AutoConnect::_classifyHandle(HTTPMethod method, String uri) { AutoConnectAux* aux = _aux.get(); while (aux) { if (aux->_uriStr == _auxUri) { + // Save the value owned by each element contained in the POST body + // of a current HTTP request to AutoConnectElements. aux->_storeElements(_webServer.get()); break; } @@ -810,11 +813,12 @@ bool AutoConnect::_classifyHandle(HTTPMethod method, String uri) { // Here, classify requested uri if (uri == _uri) { - AC_DBG_DUMB(", already allocated\n"); + AC_DBG_DUMB(",already allocated\n"); return true; // The response page already exists. } // Dispose decrepit page + _prevUri = _uri; // Save current uri for the upload request _purgePages(); // Create the page dynamically @@ -824,15 +828,31 @@ bool AutoConnect::_classifyHandle(HTTPMethod method, String uri) { _currentPageElement = _aux->_setupPage(uri); } if (_currentPageElement != nullptr) { - AC_DBG_DUMB(", generated:%s", uri.c_str()); + AC_DBG_DUMB(",generated:%s", uri.c_str()); _uri = uri; _responsePage->addElement(*_currentPageElement); _responsePage->setUri(_uri.c_str()); } - AC_DBG_DUMB(", %s\n", _currentPageElement != nullptr ? "allocated" : "ignored"); + AC_DBG_DUMB(",%s\n", _currentPageElement != nullptr ? " allocated" : "ignored"); return _currentPageElement != nullptr ? true : false; } +/** + * A wrapper of the upload function for the WebServerClass. Invokes the + * upload function of the AutoConnectAux which has a destination URI. + */ +void AutoConnect::_handleUpload(const String& requestUri, const HTTPUpload& upload) { + AutoConnectAux* aux = _aux.get(); + while (aux) { + if (aux->_uriStr == requestUri) { + aux->upload(_prevUri, upload); + // aux->upload(requestUri, upload); + break; + } + aux = aux->_next.get(); + } +} + /** * Purge allocated pages. */ diff --git a/src/AutoConnect.h b/src/AutoConnect.h index e501753..17c6bab 100644 --- a/src/AutoConnect.h +++ b/src/AutoConnect.h @@ -216,6 +216,7 @@ class AutoConnect { bool _loadAvailCredential(void); void _stopPortal(void); bool _classifyHandle(HTTPMethod mothod, String uri); + void _handleUpload(const String& requestUri, const HTTPUpload& upload); void _purgePages(void); virtual PageElement* _setupPage(String uri); #ifdef AUTOCONNECT_USE_JSON @@ -262,6 +263,7 @@ class AutoConnect { /** Extended pages made up with AutoConnectAux */ std::unique_ptr _aux; String _auxUri; /**< Last accessed AutoConnectAux */ + String _prevUri; /**< Previous generated page uri */ /** Saved configurations */ AutoConnectConfig _apConfig; diff --git a/src/AutoConnectAux.cpp b/src/AutoConnectAux.cpp index 33f37c9..c3328cf 100644 --- a/src/AutoConnectAux.cpp +++ b/src/AutoConnectAux.cpp @@ -9,6 +9,7 @@ #include #include "AutoConnect.h" #include "AutoConnectAux.h" +#include "AutoConnectUploadImpl.h" #include "AutoConnectElement.h" #include "AutoConnectElementBasisImpl.h" #ifdef AUTOCONNECT_USE_JSON @@ -204,6 +205,84 @@ bool AutoConnectAux::setElementValue(const String& name, std::vector con return rc; } +/** + * The upload function that overrides the RequestHandler class + * attached with ESP8266WebServer. + * This function invokes the upload handler registered by the onUpload + * function which will be implemented by the user sketch. + */ +void AutoConnectAux::upload(const String& requestUri, const HTTPUpload& upload) { + if (upload.status == UPLOAD_FILE_START) { + AC_DBG("%s requests upload to %s\n", requestUri.c_str(), _uriStr.c_str()); + // Selects a valid upload handler before uploading starts. + // Identify AutoConnectFile with the current upload request and + // save the value and mimeType attributes. + AC_DBG("ACFile %s ", upload.name.c_str()); + String logContext = "missing"; + + AutoConnectElementVT addons; + AutoConnectAux* aux = _ac->_aux.get(); + while (aux) { + if (aux->_uriStr == requestUri) { + addons = aux->_addonElm; + break; + } + aux = aux->_next.get(); + } + + _currentUpload = nullptr; + for (AutoConnectElement& elm : addons) { + if (elm.typeOf() == AC_File) { + _currentUpload = reinterpret_cast(&elm); + // Reset previous value + _currentUpload->value = String(""); + _currentUpload->mimeType = String(""); + _currentUpload->size = 0; + // Overwrite with current upload request + if (upload.name.equalsIgnoreCase(_currentUpload->name)) { + _currentUpload->value = upload.filename; + _currentUpload->mimeType = upload.type; + logContext = "accepted " + _currentUpload->value; + break; + } + } + } + AC_DBG_DUMB("%s, handler ", logContext.c_str()); + + // If the current upload request is AutoConnectFile without default + // AutoConnectUpload (i.e. the store attribute is AC_File_Ex), + // enable the user-owned upload handler activated by the onUpload. + _upload = nullptr; + if (_currentUpload) + if (_currentUpload->attach(_currentUpload->store)) { + _upload = std::bind(&AutoConnectUploadHandler::upload, _currentUpload->upload(), std::placeholders::_1, std::placeholders::_2); + AC_DBG_DUMB("attached(%d)\n", (int)_currentUpload->store); + } + + if (!_upload) { + if (_uploadHandler) { + _upload = _uploadHandler; + AC_DBG_DUMB("enabled\n"); + } + else + AC_DBG_DUMB("missing\n"); + } + } + + // Invokes upload handler + if (_upload) { + _upload(requestUri, upload); + if (_currentUpload) + _currentUpload->size = upload.totalSize; + // Upload ended, purge handler + if (upload.status == UPLOAD_FILE_END || upload.status == UPLOAD_FILE_ABORTED) { + if (_currentUpload) + _currentUpload->detach(); + AC_DBG("%ld bytes uploaded\n", upload.totalSize); + } + } +} + /** * Concatenates subsequent AutoConnectAux pages starting from oneself * to the chain list. @@ -359,9 +438,17 @@ PageElement* AutoConnectAux::_setupPage(const String& uri) { * @param webServer A pointer to the class object of WebServerClass */ void AutoConnectAux::_storeElements(WebServerClass* webServer) { + // Retrieve each element value, Overwrites the value of all cataloged + // AutoConnectElements with arguments inherited from last http request. for (AutoConnectElement& elm : _addonElm) { - // Overwrite the value of all cataloged AutoConnectElements with - // arguments inherited from the last http request. + + // The POST body does not contain the value of the AutoConnectFile, + // so it can not be obtained with the WebServerClass::arg function. + // The AutoConnectFile value will be restored from least recent + // upload request. + if (elm.typeOf() == AC_File) + continue; + // Relies on AutoConnectRadio, it restores to false at the being // because the checkbox argument will not pass if it is not checked. if (elm.typeOf() == AC_Checkbox) @@ -438,7 +525,7 @@ template<> AutoConnectFileBasis& AutoConnectAux::getElement(const String& name) { AutoConnectElement* elm = getElement(name); if (elm) { - if (elm->typeOf() == AC_Input) + if (elm->typeOf() == AC_File) return *(reinterpret_cast(elm)); else AC_DBG("Element<%s> type mismatch<%d>\n", name.c_str(), elm->typeOf()); diff --git a/src/AutoConnectAux.h b/src/AutoConnectAux.h index 19b9f5e..2a1d0b4 100644 --- a/src/AutoConnectAux.h +++ b/src/AutoConnectAux.h @@ -47,7 +47,7 @@ typedef enum { class AutoConnectAux : public PageBuilder { public: explicit AutoConnectAux(const String& uri = String(""), const String& title = String(""), const bool menu = true, const AutoConnectElementVT addons = AutoConnectElementVT()) : - _title(title), _menu(menu), _uriStr(String(uri)), _addonElm(addons) { _uri = _uriStr.c_str(); _next.release(); _ac.release(); } + _title(title), _menu(menu), _uriStr(String(uri)), _addonElm(addons), _handler(nullptr), _order(AC_EXIT_AHEAD), _uploadHandler(nullptr) { _uri = _uriStr.c_str(); _next.release(); _ac.release(); } ~AutoConnectAux(); void add(AutoConnectElement& addon); /**< Add an element to the auxiliary page */ void add(AutoConnectElementVT addons); /**< Add the element set to the auxiliary page */ @@ -61,6 +61,7 @@ class AutoConnectAux : public PageBuilder { bool setElementValue(const String& name, std::vector const& values); /**< Set values collection to specified element */ void setTitle(const String& title) { _title = title; } /**< Set a title of the auxiliary page */ void on(const AuxHandlerFunctionT handler, const AutoConnectExitOrder_t order = AC_EXIT_AHEAD) { _handler = handler; _order = order; } /**< Set user handler */ + void onUpload(PageBuilder::UploadFuncT uploadFunc) override { _uploadHandler = uploadFunc; } #ifdef AUTOCONNECT_USE_JSON bool load(const String& in); /**< Load whole elements to AutoConnectAux Page */ @@ -73,6 +74,7 @@ class AutoConnectAux : public PageBuilder { #endif // !AUTOCONNECT_USE_JSON protected: + void upload(const String& requestUri, const HTTPUpload& upload); /**< Uploader wrapper */ void _concat(AutoConnectAux& aux); /**< Make up chain of AutoConnectAux */ void _join(AutoConnect& ac); /**< Make a link to AutoConnect */ PageElement* _setupPage(const String& uri); /**< AutoConnectAux page builder */ @@ -100,7 +102,8 @@ class AutoConnectAux : public PageBuilder { std::unique_ptr _ac; /**< Hosted AutoConnect instance */ AuxHandlerFunctionT _handler; /**< User sketch callback function when AutoConnectAux page requested. */ AutoConnectExitOrder_t _order; /**< The order in which callback functions are called. */ - + PageBuilder::UploadFuncT _uploadHandler; /**< The AutoConnectFile corresponding to current upload */ + AutoConnectFile* _currentUpload; /**< AutoConnectFile handling the current upload */ static const char _PAGE_AUX[] PROGMEM; /**< Auxiliary page template */ // Protected members can be used from AutoConnect which handles AutoConnectAux pages. diff --git a/src/AutoConnectDefs.h b/src/AutoConnectDefs.h index e790dda..cfb14dc 100644 --- a/src/AutoConnectDefs.h +++ b/src/AutoConnectDefs.h @@ -127,6 +127,23 @@ #define AUTOCONNECT_CONTENTBUFFER_SIZE 0 #endif // !AUTOCONNECT_CONTENTBUFFER_SIZE +// SD pin assignment for AutoConnectFile +#ifndef AUTOCONNECT_SD_CS +#if defined(ARDUINO_ARCH_ESP8266) +#define AUTOCONNECT_SD_CS SD_CHIP_SELECT_PIN +#elif defined(ARDUINO_ARCH_ESP32) +#define AUTOCONNECT_SD_CS SS +#endif +#endif // !AUTOCONNECT_SD_CS + +// SPI transfer speed for SD +#ifndef AUTOCONNECT_SD_SPEED +#if defined(ARDUINO_ARCH_ESP8266) +#define AUTOCONNECT_SD_SPEED SPI_HALF_SPEED +#elif defined(ARDUINO_ARCH_ESP32) +#define AUTOCONNECT_SD_SPEED 4000000 +#endif +#endif // !AUTOCONNECT_SD_SPEED // Explicitly avoiding unused warning with token handler of PageBuilder #define AC_UNUSED(expr) do { (void)(expr); } while (0) diff --git a/src/AutoConnectElementBasis.h b/src/AutoConnectElementBasis.h index 6c18cba..dd7fb7c 100644 --- a/src/AutoConnectElementBasis.h +++ b/src/AutoConnectElementBasis.h @@ -12,6 +12,7 @@ #include #include +#include "AutoConnectUpload.h" typedef enum { AC_Button, @@ -31,6 +32,12 @@ typedef enum { AC_Vertical } ACArrange_t; /**< The element arrange order */ +typedef enum { + AC_File_FS = 0, + AC_File_SD, + AC_File_Ext +} ACFile_t; /**< AutoConnectFile media type */ + /** * AutoConnectAux element base. * Placed a raw text that can be added by user sketch. @@ -101,13 +108,23 @@ class AutoConnectCheckboxBasis : virtual public AutoConnectElementBasis { */ class AutoConnectFileBasis : virtual public AutoConnectElementBasis { public: - explicit AutoConnectFileBasis(const char* name = "", const char* value= "", const char* label = "") : AutoConnectElementBasis(name, value), label(String(label)) { + explicit AutoConnectFileBasis(const char* name = "", const char* value = "", const char* label = "", const ACFile_t store = AC_File_FS) : AutoConnectElementBasis(name, value), label(String(label)), store(store) { _type = AC_File; + _upload.reset(); } virtual ~AutoConnectFileBasis() {} const String toHTML(void) const override; + bool attach(const ACFile_t store); + void detach(void) { _upload.reset(); } + AutoConnectUploadHandler* upload(void) const { return _upload.get(); } - String label; /**< A label for a subsequent input box */ + String label; /**< A label for a subsequent input box */ + ACFile_t store; /**< Type of file store */ + String mimeType; /**< Uploading file mime type string */ + size_t size; /**< Total uploaded bytes */ + + protected: + std::unique_ptr _upload; }; /** diff --git a/src/AutoConnectElementBasisImpl.h b/src/AutoConnectElementBasisImpl.h index 5baf08e..e6f6fa1 100644 --- a/src/AutoConnectElementBasisImpl.h +++ b/src/AutoConnectElementBasisImpl.h @@ -62,6 +62,32 @@ const String AutoConnectFileBasis::toHTML(void) const { return html; } +/** + * Instantiate the upload handler with the specified store type. + * @param store An enumuration value of ACFile_t + */ +bool AutoConnectFileBasis::attach(const ACFile_t store) { + AutoConnectUploadFS* handlerFS; + AutoConnectUploadSD* handlerSD; + + // Release previous handler + detach(); + // Classify a handler type and create the corresponding handler + switch (store) { + case AC_File_FS: + handlerFS = new AutoConnectUploadFS(SPIFFS); + _upload.reset(reinterpret_cast(handlerFS)); + break; + case AC_File_SD: + handlerSD = new AutoConnectUploadSD(SD); + _upload.reset(reinterpret_cast(handlerSD)); + break; + case AC_File_Ext: + break; + } + return _upload != false; +} + /** * Generate an HTML element. * If the value member is contained, it is reflected in the placeholder diff --git a/src/AutoConnectElementJson.h b/src/AutoConnectElementJson.h index d9e4a63..88b0720 100644 --- a/src/AutoConnectElementJson.h +++ b/src/AutoConnectElementJson.h @@ -13,32 +13,36 @@ #include "AutoConnectElementBasis.h" #include -#define AUTOCONNECT_JSON_KEY_ACTION "action" -#define AUTOCONNECT_JSON_KEY_ARRANGE "arrange" -#define AUTOCONNECT_JSON_KEY_CHECKED "checked" -#define AUTOCONNECT_JSON_KEY_ELEMENT "element" -#define AUTOCONNECT_JSON_KEY_HORIZONTAL "horizontal" -#define AUTOCONNECT_JSON_KEY_LABEL "label" -#define AUTOCONNECT_JSON_KEY_MENU "menu" -#define AUTOCONNECT_JSON_KEY_NAME "name" -#define AUTOCONNECT_JSON_KEY_OPTION "option" -#define AUTOCONNECT_JSON_KEY_PATTERN "pattern" -#define AUTOCONNECT_JSON_KEY_PLACEHOLDER "placeholder" -#define AUTOCONNECT_JSON_KEY_STYLE "style" -#define AUTOCONNECT_JSON_KEY_TITLE "title" -#define AUTOCONNECT_JSON_KEY_TYPE "type" -#define AUTOCONNECT_JSON_KEY_URI "uri" -#define AUTOCONNECT_JSON_KEY_VALUE "value" -#define AUTOCONNECT_JSON_KEY_VERTICAL "vertical" -#define AUTOCONNECT_JSON_TYPE_ACBUTTON "ACButton" -#define AUTOCONNECT_JSON_TYPE_ACCHECKBOX "ACCheckBox" -#define AUTOCONNECT_JSON_TYPE_ACELEMENT "ACElement" -#define AUTOCONNECT_JSON_TYPE_ACFILE "ACFile" -#define AUTOCONNECT_JSON_TYPE_ACINPUT "ACInput" -#define AUTOCONNECT_JSON_TYPE_ACRADIO "ACRadio" -#define AUTOCONNECT_JSON_TYPE_ACSELECT "ACSelect" -#define AUTOCONNECT_JSON_TYPE_ACSUBMIT "ACSubmit" -#define AUTOCONNECT_JSON_TYPE_ACTEXT "ACText" +#define AUTOCONNECT_JSON_KEY_ACTION "action" +#define AUTOCONNECT_JSON_KEY_ARRANGE "arrange" +#define AUTOCONNECT_JSON_KEY_CHECKED "checked" +#define AUTOCONNECT_JSON_KEY_ELEMENT "element" +#define AUTOCONNECT_JSON_KEY_LABEL "label" +#define AUTOCONNECT_JSON_KEY_MENU "menu" +#define AUTOCONNECT_JSON_KEY_NAME "name" +#define AUTOCONNECT_JSON_KEY_OPTION "option" +#define AUTOCONNECT_JSON_KEY_PATTERN "pattern" +#define AUTOCONNECT_JSON_KEY_PLACEHOLDER "placeholder" +#define AUTOCONNECT_JSON_KEY_STORE "store" +#define AUTOCONNECT_JSON_KEY_STYLE "style" +#define AUTOCONNECT_JSON_KEY_TITLE "title" +#define AUTOCONNECT_JSON_KEY_TYPE "type" +#define AUTOCONNECT_JSON_KEY_URI "uri" +#define AUTOCONNECT_JSON_KEY_VALUE "value" +#define AUTOCONNECT_JSON_TYPE_ACBUTTON "ACButton" +#define AUTOCONNECT_JSON_TYPE_ACCHECKBOX "ACCheckBox" +#define AUTOCONNECT_JSON_TYPE_ACELEMENT "ACElement" +#define AUTOCONNECT_JSON_TYPE_ACFILE "ACFile" +#define AUTOCONNECT_JSON_TYPE_ACINPUT "ACInput" +#define AUTOCONNECT_JSON_TYPE_ACRADIO "ACRadio" +#define AUTOCONNECT_JSON_TYPE_ACSELECT "ACSelect" +#define AUTOCONNECT_JSON_TYPE_ACSUBMIT "ACSubmit" +#define AUTOCONNECT_JSON_TYPE_ACTEXT "ACText" +#define AUTOCONNECT_JSON_VALUE_EXTERNAL "external" +#define AUTOCONNECT_JSON_VALUE_FS "fs" +#define AUTOCONNECT_JSON_VALUE_HORIZONTAL "horizontal" +#define AUTOCONNECT_JSON_VALUE_SD "sd" +#define AUTOCONNECT_JSON_VALUE_VERTICAL "vertical" /** * AutoConnectAux element base with handling with JSON object. @@ -113,13 +117,15 @@ class AutoConnectCheckboxJson : public AutoConnectElementJson, public AutoConnec * @param value A string value entered by the selected file name. * @param label A label string that follows file-select box, optionally. * The label is placed in front of file-select box. + * @param store An enumuration value of store type. */ class AutoConnectFileJson : public AutoConnectElementJson, public AutoConnectFileBasis { public: - explicit AutoConnectFileJson(const char* name = "", const char* value= "", const char* label = "") { + explicit AutoConnectFileJson(const char* name = "", const char* value= "", const char* label = "", const ACFile_t store = AC_File_FS) { AutoConnectFileBasis::name = String(name); AutoConnectFileBasis::value = String(value); AutoConnectFileBasis::label = String(label); + AutoConnectFileBasis::store = store; } ~AutoConnectFileJson() {} size_t getObjectSize(void) const override; diff --git a/src/AutoConnectElementJsonImpl.h b/src/AutoConnectElementJsonImpl.h index cef5ada..3ef0394 100644 --- a/src/AutoConnectElementJsonImpl.h +++ b/src/AutoConnectElementJsonImpl.h @@ -145,7 +145,7 @@ void AutoConnectCheckboxJson::serialize(JsonObject& json) { * @return An object size for JsonBuffer. */ size_t AutoConnectFileJson::getObjectSize() const { - return AutoConnectElementJson::getObjectSize() + JSON_OBJECT_SIZE(1); + return AutoConnectElementJson::getObjectSize() + JSON_OBJECT_SIZE(2); } /** @@ -160,6 +160,19 @@ bool AutoConnectFileJson::loadMember(const JsonObject& json) { _setMember(json); if (json.containsKey(F(AUTOCONNECT_JSON_KEY_LABEL))) label = json.get(F(AUTOCONNECT_JSON_KEY_LABEL)); + if (json.containsKey(F(AUTOCONNECT_JSON_KEY_STORE))) { + String media = json.get(F(AUTOCONNECT_JSON_KEY_STORE)); + if (media.equalsIgnoreCase(F(AUTOCONNECT_JSON_VALUE_FS))) + store = AC_File_FS; + else if (media.equalsIgnoreCase(F(AUTOCONNECT_JSON_VALUE_SD))) + store = AC_File_SD; + else if (media.equalsIgnoreCase(F(AUTOCONNECT_JSON_VALUE_EXTERNAL))) + store = AC_File_Ext; + else { + AC_DBG("Failed to load %s element, unknown %s\n", name.c_str(), media.c_str()); + return false; + } + } return true; } return false; @@ -174,6 +187,17 @@ void AutoConnectFileJson::serialize(JsonObject& json) { json.set(F(AUTOCONNECT_JSON_KEY_TYPE), F(AUTOCONNECT_JSON_TYPE_ACFILE)); json.set(F(AUTOCONNECT_JSON_KEY_VALUE), value); json.set(F(AUTOCONNECT_JSON_KEY_LABEL), label); + switch (store) { + case AC_File_FS: + json.set(F(AUTOCONNECT_JSON_KEY_STORE), AUTOCONNECT_JSON_VALUE_FS); + break; + case AC_File_SD: + json.set(F(AUTOCONNECT_JSON_KEY_STORE), AUTOCONNECT_JSON_VALUE_SD); + break; + case AC_File_Ext: + json.set(F(AUTOCONNECT_JSON_KEY_STORE), AUTOCONNECT_JSON_VALUE_EXTERNAL); + break; + } } /** @@ -242,10 +266,14 @@ bool AutoConnectRadioJson::loadMember(const JsonObject& json) { checked = static_cast(json.get(F(AUTOCONNECT_JSON_KEY_CHECKED))); if (json.containsKey(F(AUTOCONNECT_JSON_KEY_ARRANGE))) { String arrange = json.get(F(AUTOCONNECT_JSON_KEY_ARRANGE)); - if (arrange.equalsIgnoreCase(F(AUTOCONNECT_JSON_KEY_VERTICAL))) + if (arrange.equalsIgnoreCase(F(AUTOCONNECT_JSON_VALUE_VERTICAL))) order = AC_Vertical; - else if (arrange.equalsIgnoreCase(F(AUTOCONNECT_JSON_KEY_HORIZONTAL))) + else if (arrange.equalsIgnoreCase(F(AUTOCONNECT_JSON_VALUE_HORIZONTAL))) order = AC_Horizontal; + else { + AC_DBG("Failed to load %s element, unknown %s\n", name.c_str(), arrange.c_str()); + return false; + } } if (json.containsKey(F(AUTOCONNECT_JSON_KEY_VALUE))) { empty(); @@ -271,10 +299,10 @@ void AutoConnectRadioJson::serialize(JsonObject& json) { values.add(v); switch (order) { case AC_Horizontal: - json.set(F(AUTOCONNECT_JSON_KEY_ARRANGE), AUTOCONNECT_JSON_KEY_HORIZONTAL); + json.set(F(AUTOCONNECT_JSON_KEY_ARRANGE), AUTOCONNECT_JSON_VALUE_HORIZONTAL); break; case AC_Vertical: - json.set(F(AUTOCONNECT_JSON_KEY_ARRANGE), AUTOCONNECT_JSON_KEY_VERTICAL); + json.set(F(AUTOCONNECT_JSON_KEY_ARRANGE), AUTOCONNECT_JSON_VALUE_VERTICAL); break; } json.set(F(AUTOCONNECT_JSON_KEY_CHECKED), checked); diff --git a/src/AutoConnectUpload.h b/src/AutoConnectUpload.h new file mode 100644 index 0000000..2ccbc9d --- /dev/null +++ b/src/AutoConnectUpload.h @@ -0,0 +1,38 @@ +/** + * The upload wrapper base class definition and the default up-loader + * class declarations. + * @file AutoConnectUpload.h + * @author hieromon@gmail.com + * @version 0.9.8 + * @date 2019-03-19 + * @copyright MIT license. + */ + +#ifndef _AUTOCONNECTUPLOAD_H_ +#define _AUTOCONNECTUPLOAD_H_ + +#if defined(ARDUINO_ARCH_ESP8266) +#include +#include +#elif defined(ARDUINO_ARCH_ESP32) +#include +#include +#endif + +/** + * Uploader base class. This class is a wrapper for the AutoConnectUpload + * class, and only the upload member function is implemented. + */ +class AutoConnectUploadHandler { + public: + explicit AutoConnectUploadHandler() {} + virtual ~AutoConnectUploadHandler() {} + virtual void upload(const String& requestUri, const HTTPUpload& upload); + +protected: + virtual bool _open(const char* filename, const char* mode) = 0; + virtual size_t _write(const uint8_t *buf, size_t size) = 0; + virtual void _close(void) = 0; +}; + +#endif // !_AUTOCONNECTUPLOAD_H_ diff --git a/src/AutoConnectUploadImpl.h b/src/AutoConnectUploadImpl.h new file mode 100644 index 0000000..98aedb0 --- /dev/null +++ b/src/AutoConnectUploadImpl.h @@ -0,0 +1,143 @@ +/** + * The default upload handler implementation. + * @file AutoConnectUploadImpl.h + * @author hieromon@gmail.com + * @version 0.9.8 + * @date 2019-03-19 + * @copyright MIT license. + */ + +#ifndef _AUTOCONNECTUPLOADIMPL_H_ +#define _AUTOCONNECTUPLOADIMPL_H_ + +#if defined(ARDUINO_ARCH_ESP8266) +#include +#elif defined(ARDUINO_ARCH_ESP32) +#include +#include +#endif +#include +#include +#define FS_NO_GLOBALS +#include + +// Types branching to be code commonly for the file system classes with +// ESP8266 and ESP32. +#if defined(ARDUINO_ARCH_ESP8266) +typedef fs::FS SPIFFST; // SPIFFS:File system class +typedef fs::File SPIFileT; // SPIFFS:File class +typedef SDClass SDClassT; // SD:File system class +typedef File SDFileT; // SD:File class +#elif defined(ARDUINO_ARCH_ESP32) +typedef fs::SPIFFSFS SPIFFST; +typedef fs::File SPIFileT; +typedef fs::SDFS SDClassT; +typedef SDFile SDFileT; +#endif + +#include "AutoConnectDefs.h" +#include "AutoConnectUpload.h" + +/** + * Handles the default upload process depending on the upload status. + * This handler function supports the status of UPLOAD_FILE_START, + * UPLOAD_FILE_WRITE, UPLOAD_FILE_END and calls open, write and + * close processing respectively. + * @param requestUri A reference to the upload request uri. + * @param upload A reference of HTTPUpload entity. + */ +void AutoConnectUploadHandler::upload(const String& requestUri, const HTTPUpload& upload) { + AC_UNUSED(requestUri); + switch (upload.status) { + case UPLOAD_FILE_START: { + String absFilename = "/" + upload.filename; + (void)_open(absFilename.c_str(), "w"); + break; + } + case UPLOAD_FILE_WRITE: + (void)_write(upload.buf, upload.currentSize); + break; + case UPLOAD_FILE_ABORTED: + case UPLOAD_FILE_END: + _close(); + break; + } +} + +// Default handler for uploading to the standard SPIFFS class embedded in the core. +class AutoConnectUploadFS : public AutoConnectUploadHandler { + public: + explicit AutoConnectUploadFS(SPIFFST& media) : _media(&media) {} + ~AutoConnectUploadFS() { _close(); } + + protected: + bool _open(const char* filename, const char* mode) override { +#if defined(ARDUINO_ARCH_ESP8266) + if (_media->begin()) { +#elif defined(ARDUINO_ARCH_ESP32) + if (_media->begin(true)) { +#endif + _file = _media->open(filename, mode); + return _file != false; + } + return false; + } + + size_t _write(const uint8_t* buf, size_t size) override { + if (_file) + return _file.write(buf, size); + else + return -1; + } + + void _close(void) override { + if (_file) + _file.close(); + _media->end(); + } + + SPIFFST* _media; + SPIFileT _file; +}; + +// Default handler for uploading to the standard SD class embedded in the core. +class AutoConnectUploadSD : public AutoConnectUploadHandler { + public: + explicit AutoConnectUploadSD(SDClassT& media, const uint8_t cs = AUTOCONNECT_SD_CS, const uint32_t speed = AUTOCONNECT_SD_SPEED) : _media(&media), _cs(cs), _speed(speed) {} + ~AutoConnectUploadSD() { _close(); } + + protected: + bool _open(const char* filename, const char* mode) override { +#if defined(ARDUINO_ARCH_ESP8266) + if (_media->begin(_cs, _speed)) { + uint8_t oflag = *mode == 'w' ? FILE_WRITE : FILE_READ; +#elif defined(ARDUINO_ARCH_ESP32) + if (_media->begin(_cs, SPI, _speed)) { + const char* oflag = mode; +#endif + _file = _media->open(filename, oflag); + return _file != false; + } + return false; + } + + size_t _write(const uint8_t* buf, size_t size) override { + if (_file) + return _file.write(buf, size); + else + return -1; + } + + void _close(void) override { + if (_file) + _file.close(); + _media->end(); + } + + SDClassT* _media; + SDFileT _file; + uint8_t _cs; + uint8_t _speed; +}; + +#endif // !_AUTOCONNECTUPLOADIMPL_H_