X Tutup
/** * Example for the ESP32 HTTP(S) Webserver * * IMPORTANT NOTE: * This example is a bit more complex than the other ones, so be careful to * follow all steps. * * Make sure to check out the more basic examples like Static-Page to understand * the fundamental principles of the API before proceeding with this sketch. * * To run this script, you need to * 1) Enter your WiFi SSID and PSK below this comment * 2) Install the SPIFFS File uploader into your Arduino IDE to be able to * upload static data to the webserver. * Follow the instructions at: * https://github.com/me-no-dev/arduino-esp32fs-plugin * 3) Upload the static files from the data/ directory of the example to your * module's SPIFFs by using "ESP32 Sketch Data Upload" from the tools menu. * If you face any problems, read the description of the libraray mentioned * above. * Note: If mounting SPIFFS fails, the script will wait for a serial connection * (open your serial monitor!) and ask if it should format the SPIFFS partition. * You may need this before uploading the data * Note: Make sure to select a partition layout that allows for SPIFFS in the * boards menu * 4) Have the ArduinoJSON library installed and available. (Tested with Version 5.13.4) * You'll find it at: * https://arduinojson.org/ * * This script will install an HTTPS Server on your ESP32 with the following * functionalities: * - Serve static files from the SPIFFS's data/public directory * - Provide a REST API at /api to receive the asynchronous http requests * - /api/uptime provides access to the current system uptime * - /api/events allows to register or delete events to turn PINs on/off * at certain times. * - Use Arduino JSON for body parsing and generation of responses. * - The certificate is generated on first run and stored to the SPIFFS in * the cert directory (so that the client cannot retrieve the private key) */ // TODO: Configure your WiFi here #define WIFI_SSID "" #define WIFI_PSK "" // We will use wifi #include // We will use SPIFFS and FS #include #include // We use JSON as data format. Make sure to have the lib available #include // Working with c++ strings #include // Define the name of the directory for public files in the SPIFFS parition #define DIR_PUBLIC "/public" // We need to specify some content-type mapping, so the resources get delivered with the // right content type and are displayed correctly in the browser char contentTypes[][2][32] = { {".html", "text/html"}, {".css", "text/css"}, {".js", "application/javascript"}, {".json", "application/json"}, {".png", "image/png"}, {".jpg", "image/jpg"}, {"", ""} }; // Includes for the server #include #include #include #include #include // The HTTPS Server comes in a separate namespace. For easier use, include it here. using namespace httpsserver; SSLCert * getCertificate(); void handleSPIFFS(HTTPRequest * req, HTTPResponse * res); void handleGetUptime(HTTPRequest * req, HTTPResponse * res); void handleGetEvents(HTTPRequest * req, HTTPResponse * res); void handlePostEvent(HTTPRequest * req, HTTPResponse * res); void handleDeleteEvent(HTTPRequest * req, HTTPResponse * res); // We use the following struct to store GPIO events: #define MAX_EVENTS 20 struct { // is this event used (events that have been run will be set to false) bool active; // when should it be run? unsigned long time; // which GPIO should be changed? int gpio; // and to which state? int state; } events[MAX_EVENTS]; // We just create a reference to the server here. We cannot call the constructor unless // we have initialized the SPIFFS and read or created the certificate HTTPSServer * secureServer; void setup() { // For logging Serial.begin(115200); // Set the pins that we will use as output pins pinMode(25, OUTPUT); pinMode(26, OUTPUT); pinMode(27, OUTPUT); pinMode(32, OUTPUT); pinMode(33, OUTPUT); // Try to mount SPIFFS without formatting on failure if (!SPIFFS.begin(false)) { // If SPIFFS does not work, we wait for serial connection... while(!Serial); delay(1000); // Ask to format SPIFFS using serial interface Serial.print("Mounting SPIFFS failed. Try formatting? (y/n): "); while(!Serial.available()); Serial.println(); // If the user did not accept to try formatting SPIFFS or formatting failed: if (Serial.read() != 'y' || !SPIFFS.begin(true)) { Serial.println("SPIFFS not available. Stop."); while(true); } Serial.println("SPIFFS has been formated."); } Serial.println("SPIFFS has been mounted."); // Now that SPIFFS is ready, we can create or load the certificate SSLCert *cert = getCertificate(); if (cert == NULL) { Serial.println("Could not load certificate. Stop."); while(true); } // Initialize event structure: for(int i = 0; i < MAX_EVENTS; i++) { events[i].active = false; events[i].gpio = 0; events[i].state = LOW; events[i].time = 0; } // Connect to WiFi Serial.println("Setting up WiFi"); WiFi.begin(WIFI_SSID, WIFI_PSK); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); } Serial.print("Connected. IP="); Serial.println(WiFi.localIP()); // Create the server with the certificate we loaded before secureServer = new HTTPSServer(cert); // We register the SPIFFS handler as the default node, so every request that does // not hit any other node will be redirected to the file system. ResourceNode * spiffsNode = new ResourceNode("", "", &handleSPIFFS); secureServer->setDefaultNode(spiffsNode); // Add a handler that serves the current system uptime at GET /api/uptime ResourceNode * uptimeNode = new ResourceNode("/api/uptime", "GET", &handleGetUptime); secureServer->registerNode(uptimeNode); // Add the handler nodes that deal with modifying the events: ResourceNode * getEventsNode = new ResourceNode("/api/events", "GET", &handleGetEvents); secureServer->registerNode(getEventsNode); ResourceNode * postEventNode = new ResourceNode("/api/events", "POST", &handlePostEvent); secureServer->registerNode(postEventNode); ResourceNode * deleteEventNode = new ResourceNode("/api/events/*", "DELETE", &handleDeleteEvent); secureServer->registerNode(deleteEventNode); Serial.println("Starting server..."); secureServer->start(); if (secureServer->isRunning()) { Serial.println("Server ready."); } } void loop() { // This call will let the server do its work secureServer->loop(); // Here we handle the events unsigned long now = millis() / 1000; for (int i = 0; i < MAX_EVENTS; i++) { // Only handle active events: if (events[i].active) { // Only if the counter has recently been exceeded if (events[i].time < now) { // Apply the state change digitalWrite(events[i].gpio, events[i].state); // Deactivate the event so it doesn't fire again events[i].active = false; } } } // Other code would go here... delay(1); } /** * This function will either read the certificate and private key from SPIFFS or * create a self-signed certificate and write it to SPIFFS for next boot */ SSLCert * getCertificate() { // Try to open key and cert file to see if they exist File keyFile = SPIFFS.open("/key.der"); File certFile = SPIFFS.open("/cert.der"); // If now, create them if (!keyFile || !certFile || keyFile.size()==0 || certFile.size()==0) { Serial.println("No certificate found in SPIFFS, generating a new one for you."); Serial.println("If you face a Guru Meditation, give the script another try (or two...)."); Serial.println("This may take up to a minute, so please stand by :)"); SSLCert * newCert = new SSLCert(); // The part after the CN= is the domain that this certificate will match, in this // case, it's esp32.local. // However, as the certificate is self-signed, your browser won't trust the server // anyway. int res = createSelfSignedCert(*newCert, KEYSIZE_1024, "CN=esp32.local,O=acme,C=DE"); if (res == 0) { // We now have a certificate. We store it on the SPIFFS to restore it on next boot. bool failure = false; // Private key keyFile = SPIFFS.open("/key.der", FILE_WRITE); if (!keyFile || !keyFile.write(newCert->getPKData(), newCert->getPKLength())) { Serial.println("Could not write /key.der"); failure = true; } if (keyFile) keyFile.close(); // Certificate certFile = SPIFFS.open("/cert.der", FILE_WRITE); if (!certFile || !certFile.write(newCert->getCertData(), newCert->getCertLength())) { Serial.println("Could not write /cert.der"); failure = true; } if (certFile) certFile.close(); if (failure) { Serial.println("Certificate could not be stored permanently, generating new certificate on reboot..."); } return newCert; } else { // Certificate generation failed. Inform the user. Serial.println("An error occured during certificate generation."); Serial.print("Error code is 0x"); Serial.println(res, HEX); Serial.println("You may have a look at SSLCert.h to find the reason for this error."); return NULL; } } else { Serial.println("Reading certificate from SPIFFS."); // The files exist, so we can create a certificate based on them size_t keySize = keyFile.size(); size_t certSize = certFile.size(); uint8_t * keyBuffer = new uint8_t[keySize]; if (keyBuffer == NULL) { Serial.println("Not enough memory to load privat key"); return NULL; } uint8_t * certBuffer = new uint8_t[certSize]; if (certBuffer == NULL) { delete[] keyBuffer; Serial.println("Not enough memory to load certificate"); return NULL; } keyFile.read(keyBuffer, keySize); certFile.read(certBuffer, certSize); // Close the files keyFile.close(); certFile.close(); Serial.printf("Read %u bytes of certificate and %u bytes of key from SPIFFS\n", certSize, keySize); return new SSLCert(certBuffer, certSize, keyBuffer, keySize); } } /** * This handler function will try to load the requested resource from SPIFFS's /public folder. * * If the method is not GET, it will throw 405, if the file is not found, it will throw 404. */ void handleSPIFFS(HTTPRequest * req, HTTPResponse * res) { // We only handle GET here if (req->getMethod() == "GET") { // Redirect / to /index.html std::string reqFile = req->getRequestString()=="/" ? "/index.html" : req->getRequestString(); // Try to open the file std::string filename = std::string(DIR_PUBLIC) + reqFile; // Check if the file exists if (!SPIFFS.exists(filename.c_str())) { // Send "404 Not Found" as response, as the file doesn't seem to exist res->setStatusCode(404); res->setStatusText("Not found"); res->println("404 Not Found"); return; } File file = SPIFFS.open(filename.c_str()); // Set length res->setHeader("Content-Length", httpsserver::intToString(file.size())); // Content-Type is guessed using the definition of the contentTypes-table defined above int cTypeIdx = 0; do { if(reqFile.rfind(contentTypes[cTypeIdx][0])!=std::string::npos) { res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); break; } cTypeIdx+=1; } while(strlen(contentTypes[cTypeIdx][0])>0); // Read the file and write it to the response uint8_t buffer[256]; size_t length = 0; do { length = file.read(buffer, 256); res->write(buffer, length); } while (length > 0); file.close(); } else { // If there's any body, discard it req->discardRequestBody(); // Send "405 Method not allowed" as response res->setStatusCode(405); res->setStatusText("Method not allowed"); res->println("405 Method not allowed"); } } /** * This function will return the uptime in seconds as JSON object: * {"uptime": 42} */ void handleGetUptime(HTTPRequest * req, HTTPResponse * res) { // Create a buffer of size 1 (pretty simple, we have just one key here) StaticJsonBuffer jsonBuffer; // Create an object at the root JsonObject& obj = jsonBuffer.createObject(); // Set the uptime key to the uptime in seconds obj["uptime"] = millis()/1000; // Set the content type of the response res->setHeader("Content-Type", "application/json"); // As HTTPResponse implements the Print interface, this works fine. Just remember // to use *, as we only have a pointer to the HTTPResponse here: obj.printTo(*res); } /** * This handler will return a JSON array of currently active events for GET /api/events */ void handleGetEvents(HTTPRequest * req, HTTPResponse * res) { // We need to calculate the capacity of the json buffer int activeEvents = 0; for(int i = 0; i < MAX_EVENTS; i++) { if (events[i].active) activeEvents++; } // For each active event, we need 1 array element with 4 objects const size_t capacity = JSON_ARRAY_SIZE(activeEvents) + activeEvents * JSON_OBJECT_SIZE(4); // DynamicJsonBuffer is created on the heap instead of the stack DynamicJsonBuffer jsonBuffer(capacity); JsonArray& arr = jsonBuffer.createArray(); for(int i = 0; i < MAX_EVENTS; i++) { if (events[i].active) { JsonObject& eventObj = arr.createNestedObject(); eventObj["gpio"] = events[i].gpio; eventObj["state"] = events[i].state; eventObj["time"] = events[i].time; // Add the index to allow delete and post to identify the element eventObj["id"] = i; } } // Print to response res->setHeader("Content-Type", "application/json"); arr.printTo(*res); } void handlePostEvent(HTTPRequest * req, HTTPResponse * res) { // We expect an object with 4 elements and add some buffer const size_t capacity = JSON_OBJECT_SIZE(4) + 180; DynamicJsonBuffer jsonBuffer(capacity); // Create buffer to read request char * buffer = new char[capacity + 1]; memset(buffer, 0, capacity+1); // Try to read request into buffer size_t idx = 0; // while "not everything read" or "buffer is full" while (!req->requestComplete() && idx < capacity) { idx += req->readChars(buffer + idx, capacity-idx); } // If the request is still not read completely, we cannot process it. if (!req->requestComplete()) { res->setStatusCode(413); res->setStatusText("Request entity too large"); res->println("413 Request entity too large"); // Clean up delete[] buffer; return; } // Parse the object JsonObject& reqObj = jsonBuffer.parseObject(buffer); // Check input data types bool dataValid = true; if (!reqObj.is("time") || !reqObj.is("gpio") || !reqObj.is("state")) { dataValid = false; } // Check actual values unsigned long eTime = 0; int eGpio = 0; int eState = LOW; if (dataValid) { eTime = reqObj["time"]; if (eTime < millis()/1000) dataValid = false; eGpio = reqObj["gpio"]; if (!(eGpio == 25 || eGpio == 26 || eGpio == 27 || eGpio == 32 || eGpio == 33)) dataValid = false; eState = reqObj["state"]; if (eState != HIGH && eState != LOW) dataValid = false; } // Clean up, we don't need the buffer any longer delete[] buffer; // If something failed: 400 if (!dataValid) { res->setStatusCode(400); res->setStatusText("Bad Request"); res->println("400 Bad Request"); return; } // Try to find an inactive event in the list to write the data to int eventID = -1; for(int i = 0; i < MAX_EVENTS && eventID==-1; i++) { if (!events[i].active) { eventID = i; events[i].gpio = eGpio; events[i].time = eTime; events[i].state = eState; events[i].active = true; } } // Check if we could store the event if (eventID>-1) { // Create a buffer for the response StaticJsonBuffer resBuffer; // Create an object at the root JsonObject& resObj = resBuffer.createObject(); // Set the uptime key to the uptime in seconds resObj["gpio"] = events[eventID].gpio; resObj["state"] = events[eventID].state; resObj["time"] = events[eventID].time; resObj["id"] = eventID; // Write the response res->setHeader("Content-Type", "application/json"); resObj.printTo(*res); } else { // We could not store the event, no free slot. res->setStatusCode(507); res->setStatusText("Insufficient storage"); res->println("507 Insufficient storage"); } } /** * This handler will delete an event (meaning: deactive the event) */ void handleDeleteEvent(HTTPRequest * req, HTTPResponse * res) { // Access the parameter from the URL. See Parameters example for more details on this ResourceParameters * params = req->getParams(); size_t eid = std::atoi(params->getPathParameter(0).c_str()); if (eid < MAX_EVENTS) { // Set the inactive flag events[eid].active = false; // And return a successful response without body res->setStatusCode(204); res->setStatusText("No Content"); } else { // Send error message res->setStatusCode(400); res->setStatusText("Bad Request"); res->println("400 Bad Request"); } }
X Tutup