diff --git a/bin/build-all.sh b/bin/build-all.sh index c27760c4..caa7c59a 100755 --- a/bin/build-all.sh +++ b/bin/build-all.sh @@ -14,7 +14,7 @@ BOARDS_ESP32="tlora-v2 tlora-v1 tlora-v2-1-1.6 tbeam heltec tbeam0.7" # FIXME note nrf52840dk build is for some reason only generating a BIN file but not a HEX file nrf52840dk-geeksville is fine BOARDS_NRF52="lora-relay-v1" -NUM_JOBS=2 +NUM_JOBS=2 || true OUTDIR=release/latest diff --git a/docs/radio-settings.md b/docs/radio-settings.md index 2bf9ccda..7af9d79e 100644 --- a/docs/radio-settings.md +++ b/docs/radio-settings.md @@ -28,3 +28,14 @@ The maximum output power for North America is +30 dBm ERP. The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. + +## Data-rates + +Various data-rates are selectable when configuring a channel and are inversely proportional to the theoretical range of the devices: + +| Channel setting | Data-rate | +|----------------------------|----------------------| +| Short range (but fast) | 21.875 kbps | +| Medium range (but fast) | 5.469 kbps | +| Long range (but slower) | 0.275 kbps | +| Very long range (but slow) | 0.183 kbps (default) | diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp new file mode 100644 index 00000000..5f79702f --- /dev/null +++ b/src/mesh/http/ContentHandler.cpp @@ -0,0 +1,1073 @@ +#include "NodeDB.h" +#include "PowerFSM.h" +#include "airtime.h" +#include "main.h" +#include "mesh/http/ContentHelper.h" +#include "mesh/http/ContentStatic.h" +#include "mesh/http/WiFiAPClient.h" +#include "power.h" +#include "sleep.h" +#include +#include +#include +#include + +#ifndef NO_ESP32 +#include "esp_task_wdt.h" +#endif + +/* + Including the esp32_https_server library will trigger a compile time error. I've + tracked it down to a reoccurrance of this bug: + https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57824 + The work around is described here: + https://forums.xilinx.com/t5/Embedded-Development-Tools/Error-with-Standard-Libaries-in-Zynq/td-p/450032 + + Long story short is we need "#undef str" before including the esp32_https_server. + - Jm Casler (jm@casler.org) Oct 2020 +*/ +#undef str + +// Includes for the https server +// https://github.com/fhessel/esp32_https_server +#include +#include +#include +#include +#include + +// The HTTPS Server comes in a separate namespace. For easier use, include it here. +using namespace httpsserver; + +#include "mesh/http/ContentHandler.h" + +// 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] = {{".txt", "text/plain"}, {".html", "text/html"}, + {".js", "text/javascript"}, {".png", "image/png"}, + {".jpg", "image/jpg"}, {".gz", "application/gzip"}, + {".gif", "image/gif"}, {".json", "application/json"}, + {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, + {".svg", "image/svg+xml"}, {"", ""}}; + +// Our API to handle messages to and from the radio. +HttpAPI webAPI; + +uint32_t numberOfRequests = 0; +uint32_t timeSpeedUp = 0; + +uint32_t getTimeSpeedUp() +{ + return timeSpeedUp; +} + +void setTimeSpeedUp() +{ + timeSpeedUp = millis(); +} + +void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer) +{ + + // For every resource available on the server, we need to create a ResourceNode + // The ResourceNode links URL and HTTP method to a handler function + + ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); + ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); + + ResourceNode *nodeHotspot = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); + ResourceNode *nodeFavicon = new ResourceNode("/favicon.ico", "GET", &handleFavicon); + ResourceNode *nodeRoot = new ResourceNode("/", "GET", &handleRoot); + ResourceNode *nodeStaticBrowse = new ResourceNode("/static", "GET", &handleStaticBrowse); + ResourceNode *nodeStaticPOST = new ResourceNode("/static", "POST", &handleStaticPost); + ResourceNode *nodeStatic = new ResourceNode("/static/*", "GET", &handleStatic); + ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); + ResourceNode *node404 = new ResourceNode("", "GET", &handle404); + ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); + ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); + ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); + ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); + ResourceNode *nodeJsonSpiffsBrowseStatic = new ResourceNode("/json/spiffs/browse/static", "GET", &handleSpiffsBrowseStatic); + ResourceNode *nodeJsonDelete = new ResourceNode("/json/spiffs/delete/static", "DELETE", &handleSpiffsDeleteStatic); + + // Secure nodes + secureServer->registerNode(nodeAPIv1ToRadioOptions); + secureServer->registerNode(nodeAPIv1ToRadio); + secureServer->registerNode(nodeAPIv1FromRadio); + secureServer->registerNode(nodeHotspot); + secureServer->registerNode(nodeFavicon); + secureServer->registerNode(nodeRoot); + secureServer->registerNode(nodeStaticBrowse); + secureServer->registerNode(nodeStaticPOST); + secureServer->registerNode(nodeStatic); + secureServer->registerNode(nodeRestart); + secureServer->registerNode(nodeFormUpload); + secureServer->registerNode(nodeJsonScanNetworks); + secureServer->registerNode(nodeJsonBlinkLED); + secureServer->registerNode(nodeJsonSpiffsBrowseStatic); + secureServer->registerNode(nodeJsonDelete); + secureServer->registerNode(nodeJsonReport); + secureServer->setDefaultNode(node404); + + secureServer->addMiddleware(&middlewareSpeedUp240); + + // Insecure nodes + insecureServer->registerNode(nodeAPIv1ToRadioOptions); + insecureServer->registerNode(nodeAPIv1ToRadio); + insecureServer->registerNode(nodeAPIv1FromRadio); + insecureServer->registerNode(nodeHotspot); + insecureServer->registerNode(nodeFavicon); + insecureServer->registerNode(nodeRoot); + insecureServer->registerNode(nodeStaticBrowse); + insecureServer->registerNode(nodeStaticPOST); + insecureServer->registerNode(nodeStatic); + insecureServer->registerNode(nodeRestart); + insecureServer->registerNode(nodeFormUpload); + insecureServer->registerNode(nodeJsonScanNetworks); + insecureServer->registerNode(nodeJsonBlinkLED); + insecureServer->registerNode(nodeJsonSpiffsBrowseStatic); + insecureServer->registerNode(nodeJsonDelete); + insecureServer->registerNode(nodeJsonReport); + insecureServer->setDefaultNode(node404); + + insecureServer->addMiddleware(&middlewareSpeedUp160); +} + +void middlewareSpeedUp240(HTTPRequest *req, HTTPResponse *res, std::function next) +{ + // We want to print the response status, so we need to call next() first. + next(); + + // Phone (or other device) has contacted us over WiFi. Keep the radio turned on. + // TODO: This should go into its own middleware layer separate from the speedup. + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + + setCpuFrequencyMhz(240); + setTimeSpeedUp(); + + numberOfRequests++; +} + +void middlewareSpeedUp160(HTTPRequest *req, HTTPResponse *res, std::function next) +{ + // We want to print the response status, so we need to call next() first. + next(); + + // Phone (or other device) has contacted us over WiFi. Keep the radio turned on. + // TODO: This should go into its own middleware layer separate from the speedup. + powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); + + // If the frequency is 240mhz, we have recently gotten a HTTPS request. + // In that case, leave the frequency where it is and just update the + // countdown timer (timeSpeedUp). + if (getCpuFrequencyMhz() != 240) { + setCpuFrequencyMhz(160); + } + setTimeSpeedUp(); + + numberOfRequests++; +} + +void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) +{ + + DEBUG_MSG("+++++++++++++++ webAPI handleAPIv1FromRadio\n"); + + /* + For documentation, see: + https://github.com/meshtastic/Meshtastic-device/wiki/HTTP-REST-API-discussion + https://github.com/meshtastic/Meshtastic-device/blob/master/docs/software/device-api.md + + Example: + http://10.10.30.198/api/v1/fromradio + */ + + // Get access to the parameters + ResourceParameters *params = req->getParams(); + + // std::string paramAll = "all"; + std::string valueAll; + + // Status code is 200 OK by default. + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/Meshtastic-protobufs/master/mesh.proto"); + + uint8_t txBuf[MAX_STREAM_BUF_SIZE]; + uint32_t len = 1; + + if (params->getQueryParameter("all", valueAll)) { + + // If all is ture, return all the buffers we have available + // to us at this point in time. + if (valueAll == "true") { + while (len) { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + // Otherwise, just return one protobuf + } else { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + // the param "all" was not spcified. Return just one protobuf + } else { + len = webAPI.getFromRadio(txBuf); + res->write(txBuf, len); + } + + DEBUG_MSG("--------------- webAPI handleAPIv1FromRadio, len %d\n", len); +} + +void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) +{ + DEBUG_MSG("+++++++++++++++ webAPI handleAPIv1ToRadio\n"); + + /* + For documentation, see: + https://github.com/meshtastic/Meshtastic-device/wiki/HTTP-REST-API-discussion + https://github.com/meshtastic/Meshtastic-device/blob/master/docs/software/device-api.md + + Example: + http://10.10.30.198/api/v1/toradio + */ + + // Status code is 200 OK by default. + + res->setHeader("Content-Type", "application/x-protobuf"); + res->setHeader("Access-Control-Allow-Headers", "Content-Type"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); + res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/Meshtastic-protobufs/master/mesh.proto"); + + if (req->getMethod() == "OPTIONS") { + res->setStatusCode(204); // Success with no content + res->print(""); + return; + } + + byte buffer[MAX_TO_FROM_RADIO_SIZE]; + size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); + + DEBUG_MSG("Received %d bytes from PUT request\n", s); + webAPI.handleToRadio(buffer, s); + + res->write(buffer, s); + DEBUG_MSG("--------------- webAPI handleAPIv1ToRadio\n"); +} + +void handleFavicon(HTTPRequest *req, HTTPResponse *res) +{ + // Set Content-Type + res->setHeader("Content-Type", "image/vnd.microsoft.icon"); + // Write data from header file + res->write(FAVICON_DATA, FAVICON_LENGTH); +} + +void handleStaticPost(HTTPRequest *req, HTTPResponse *res) +{ + // Assume POST request. Contains submitted data. + res->println("File Edited

File Edited

"); + + // The form is submitted with the x-www-form-urlencoded content type, so we need the + // HTTPURLEncodedBodyParser to read the fields. + // Note that the content of the file's content comes from a
"); + res->println(""); + res->println(""); + res->println(""); + + return; + } + + res->println("

Upload new file

"); + res->println("

This form allows you to upload files. Keep your filenames small and files under 200k.

"); + res->println("
"); + res->println("file:
"); + res->println(""); + res->println("
"); + + res->println("

All Files

"); + + File root = SPIFFS.open("/"); + if (root.isDirectory()) { + res->println(""); + + res->println(""); + res->println(""); + res->println(""); + res->println(""); + res->println(""); + res->println(""); + + File file = root.openNextFile(); + while (file) { + String filePath = String(file.name()); + if (filePath.indexOf("/static") == 0) { + res->println(""); + res->println(""); + res->println(""); + res->println(""); + res->println(""); + res->println(""); + } + + file = root.openNextFile(); + } + res->println("
File"); + res->println("Size"); + res->println("Actions"); + res->println("
"); + + if (String(file.name()).substring(1).endsWith(".gz")) { + String modifiedFile = String(file.name()).substring(1); + modifiedFile.remove((modifiedFile.length() - 3), 3); + res->print("" + String(file.name()).substring(1) + ""); + } else { + res->print("" + String(file.name()).substring(1) + + ""); + } + res->println(""); + res->print(String(file.size())); + res->println(""); + res->print("Delete "); + res->println(""); + if (!String(file.name()).substring(1).endsWith(".gz")) { + res->print("Edit"); + } + res->println("
"); + + res->print("
"); + // res->print("Total : " + String(SPIFFS.totalBytes()) + " Bytes
"); + res->print("Used : " + String(SPIFFS.usedBytes()) + " Bytes
"); + res->print("Free : " + String(SPIFFS.totalBytes() - SPIFFS.usedBytes()) + " Bytes
"); + } +} + +void handleStatic(HTTPRequest *req, HTTPResponse *res) +{ + // Get access to the parameters + ResourceParameters *params = req->getParams(); + + std::string parameter1; + // Print the first parameter value + if (params->getPathParameter(0, parameter1)) { + + std::string filename = "/static/" + parameter1; + std::string filenameGzip = "/static/" + parameter1 + ".gz"; + + if (!SPIFFS.exists(filename.c_str()) && !SPIFFS.exists(filenameGzip.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"); + res->printf("

File not found: %s

\n", filename.c_str()); + return; + } + + // Try to open the file from SPIFFS + File file; + + if (SPIFFS.exists(filename.c_str())) { + file = SPIFFS.open(filename.c_str()); + if (!file.available()) { + DEBUG_MSG("File not available - %s\n", filename.c_str()); + } + + } else if (SPIFFS.exists(filenameGzip.c_str())) { + file = SPIFFS.open(filenameGzip.c_str()); + res->setHeader("Content-Encoding", "gzip"); + if (!file.available()) { + DEBUG_MSG("File not available\n"); + } + } + + res->setHeader("Content-Length", httpsserver::intToString(file.size())); + + bool has_set_content_type = false; + // Content-Type is guessed using the definition of the contentTypes-table defined above + int cTypeIdx = 0; + do { + if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { + res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); + has_set_content_type = true; + break; + } + cTypeIdx += 1; + } while (strlen(contentTypes[cTypeIdx][0]) > 0); + + if (!has_set_content_type) { + // Set a default content type + res->setHeader("Content-Type", "application/octet-stream"); + } + + // Read the file from SPIFFS and write it to the HTTP response body + size_t length = 0; + do { + char buffer[256]; + length = file.read((uint8_t *)buffer, 256); + std::string bufferString(buffer, length); + res->write((uint8_t *)bufferString.c_str(), bufferString.size()); + } while (length > 0); + + file.close(); + + return; + + } else { + res->println("ERROR: This should not have happened..."); + } +} + +void handleFormUpload(HTTPRequest *req, HTTPResponse *res) +{ + + DEBUG_MSG("Form Upload - Disabling keep-alive\n"); + res->setHeader("Connection", "close"); + + DEBUG_MSG("Form Upload - Set frequency to 240mhz\n"); + // The upload process is very CPU intensive. Let's speed things up a bit. + setCpuFrequencyMhz(240); + + // First, we need to check the encoding of the form that we have received. + // The browser will set the Content-Type request header, so we can use it for that purpose. + // Then we select the body parser based on the encoding. + // Actually we do this only for documentary purposes, we know the form is going + // to be multipart/form-data. + DEBUG_MSG("Form Upload - Creating body parser reference\n"); + HTTPBodyParser *parser; + std::string contentType = req->getHeader("Content-Type"); + + // The content type may have additional properties after a semicolon, for exampel: + // Content-Type: text/html;charset=utf-8 + // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs + // As we're interested only in the actual mime _type_, we strip everything after the + // first semicolon, if one exists: + size_t semicolonPos = contentType.find(";"); + if (semicolonPos != std::string::npos) { + contentType = contentType.substr(0, semicolonPos); + } + + // Now, we can decide based on the content type: + if (contentType == "multipart/form-data") { + DEBUG_MSG("Form Upload - multipart/form-data\n"); + parser = new HTTPMultipartBodyParser(req); + } else { + Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); + return; + } + + res->println("File " + "Upload

File Upload

"); + + // We iterate over the fields. Any field with a filename is uploaded. + // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's + // fields only a single time. The reason for this is that it allows you to handle large requests + // which would not fit into memory. + bool didwrite = false; + + // parser->nextField() will move the parser to the next field in the request body (field meaning a + // form field, if you take the HTML perspective). After the last field has been processed, nextField() + // returns false and the while loop ends. + while (parser->nextField()) { + // For Multipart data, each field has three properties: + // The name ("name" value of the tag) + // The filename (If it was a , this is the filename on the machine of the + // user uploading it) + // The mime type (It is determined by the client. So do not trust this value and blindly start + // parsing files only if the type matches) + std::string name = parser->getFieldName(); + std::string filename = parser->getFieldFilename(); + std::string mimeType = parser->getFieldMimeType(); + // We log all three values, so that you can observe the upload on the serial monitor: + DEBUG_MSG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), + mimeType.c_str()); + + // Double check that it is what we expect + if (name != "file") { + DEBUG_MSG("Skipping unexpected field\n"); + res->println("

No file found.

"); + return; + } + + // Double check that it is what we expect + if (filename == "") { + DEBUG_MSG("Skipping unexpected field\n"); + res->println("

No file found.

"); + return; + } + + // SPIFFS limits the total lenth of a path + file to 31 characters. + if (filename.length() + 8 > 31) { + DEBUG_MSG("Uploaded filename too long!\n"); + res->println("

Uploaded filename too long! Limit of 23 characters.

"); + delete parser; + return; + } + + // You should check file name validity and all that, but we skip that to make the core + // concepts of the body parser functionality easier to understand. + std::string pathname = "/static/" + filename; + + // Create a new file on spiffs to stream the data into + File file = SPIFFS.open(pathname.c_str(), "w"); + size_t fileLength = 0; + didwrite = true; + + // With endOfField you can check whether the end of field has been reached or if there's + // still data pending. With multipart bodies, you cannot know the field size in advance. + while (!parser->endOfField()) { + esp_task_wdt_reset(); + + byte buf[512]; + size_t readLength = parser->read(buf, 512); + // DEBUG_MSG("\n\nreadLength - %i\n", readLength); + + // Abort the transfer if there is less than 50k space left on the filesystem. + if (SPIFFS.totalBytes() - SPIFFS.usedBytes() < 51200) { + file.close(); + res->println("

Write aborted! Reserving 50k on filesystem.

"); + + // enableLoopWDT(); + + delete parser; + return; + } + + // if (readLength) { + file.write(buf, readLength); + fileLength += readLength; + DEBUG_MSG("File Length %i\n", fileLength); + //} + } + // enableLoopWDT(); + + file.close(); + res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); + } + if (!didwrite) { + res->println("

Did not write any file

"); + } + res->println(""); + delete parser; +} + +void handleReport(HTTPRequest *req, HTTPResponse *res) +{ + + ResourceParameters *params = req->getParams(); + std::string content; + + if (!params->getQueryParameter("content", content)) { + content = "json"; + } + + if (content == "json") { + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + + } else { + res->setHeader("Content-Type", "text/html"); + res->println("
");
+    }
+
+    res->println("{");
+
+    res->println("\"data\": {");
+
+    res->println("\"airtime\": {");
+
+    uint32_t *logArray;
+
+    res->print("\"tx_log\": [");
+
+    logArray = airtimeReport(TX_LOG);
+    for (int i = 0; i < getPeriodsToLog(); i++) {
+        uint32_t tmp;
+        tmp = *(logArray + i);
+        res->printf("%d", tmp);
+        if (i != getPeriodsToLog() - 1) {
+            res->print(", ");
+        }
+    }
+
+    res->println("],");
+    res->print("\"rx_log\": [");
+
+    logArray = airtimeReport(RX_LOG);
+    for (int i = 0; i < getPeriodsToLog(); i++) {
+        uint32_t tmp;
+        tmp = *(logArray + i);
+        res->printf("%d", tmp);
+        if (i != getPeriodsToLog() - 1) {
+            res->print(", ");
+        }
+    }
+
+    res->println("],");
+    res->print("\"rx_all_log\": [");
+
+    logArray = airtimeReport(RX_ALL_LOG);
+    for (int i = 0; i < getPeriodsToLog(); i++) {
+        uint32_t tmp;
+        tmp = *(logArray + i);
+        res->printf("%d", tmp);
+        if (i != getPeriodsToLog() - 1) {
+            res->print(", ");
+        }
+    }
+
+    res->println("],");
+    res->printf("\"seconds_since_boot\": %u,\n", getSecondsSinceBoot());
+    res->printf("\"seconds_per_period\": %u,\n", getSecondsPerPeriod());
+    res->printf("\"periods_to_log\": %u\n", getPeriodsToLog());
+
+    res->println("},");
+
+    res->println("\"wifi\": {");
+
+    res->printf("\"web_request_count\": %d,\n", numberOfRequests);
+    res->println("\"rssi\": " + String(WiFi.RSSI()) + ",");
+
+    if (radioConfig.preferences.wifi_ap_mode || isSoftAPForced()) {
+        res->println("\"ip\": \"" + String(WiFi.softAPIP().toString().c_str()) + "\"");
+    } else {
+        res->println("\"ip\": \"" + String(WiFi.localIP().toString().c_str()) + "\"");
+    }
+
+    res->println("},");
+
+    res->println("\"memory\": {");
+    res->printf("\"heap_total\": %d,\n", ESP.getHeapSize());
+    res->printf("\"heap_free\": %d,\n", ESP.getFreeHeap());
+    res->printf("\"psram_total\": %d,\n", ESP.getPsramSize());
+    res->printf("\"psram_free\": %d,\n", ESP.getFreePsram());
+    res->println("\"spiffs_total\" : " + String(SPIFFS.totalBytes()) + ",");
+    res->println("\"spiffs_used\" : " + String(SPIFFS.usedBytes()) + ",");
+    res->println("\"spiffs_free\" : " + String(SPIFFS.totalBytes() - SPIFFS.usedBytes()));
+    res->println("},");
+
+    res->println("\"power\": {");
+    res->printf("\"battery_percent\": %u,\n", powerStatus->getBatteryChargePercent());
+    res->printf("\"battery_voltage_mv\": %u,\n", powerStatus->getBatteryVoltageMv());
+    res->printf("\"has_battery\": %s,\n", BoolToString(powerStatus->getHasBattery()));
+    res->printf("\"has_usb\": %s,\n", BoolToString(powerStatus->getHasUSB()));
+    res->printf("\"is_charging\": %s\n", BoolToString(powerStatus->getIsCharging()));
+    res->println("}");
+
+    res->println("},");
+
+    res->println("\"status\": \"ok\"");
+    res->println("}");
+}
+
+// --------
+
+void handle404(HTTPRequest *req, HTTPResponse *res)
+{
+
+    // Discard request body, if we received any
+    // We do this, as this is the default node and may also server POST/PUT requests
+    req->discardRequestBody();
+
+    // Set the response status
+    res->setStatusCode(404);
+    res->setStatusText("Not Found");
+
+    // Set content type of the response
+    res->setHeader("Content-Type", "text/html");
+
+    // Write a tiny HTTP page
+    res->println("");
+    res->println("");
+    res->println("Not Found");
+    res->println("

404 Not Found

The requested resource was not found on this server.

"); + res->println(""); +} + +/* + This supports the Apple Captive Network Assistant (CNA) Portal +*/ +void handleHotspot(HTTPRequest *req, HTTPResponse *res) +{ + DEBUG_MSG("Hotspot Request\n"); + + /* + If we don't do a redirect, be sure to return a "Success" message + otherwise iOS will have trouble detecting that the connection to the SoftAP worked. + */ + + // Status code is 200 OK by default. + // We want to deliver a simple HTML page, so we send a corresponding content type: + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + + // res->println(""); + res->println("\n"); +} + +void handleRestart(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "text/html"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + + DEBUG_MSG("***** Restarted on HTTP(s) Request *****\n"); + res->println("Restarting"); + + ESP.restart(); +} + +void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "POST"); + + ResourceParameters *params = req->getParams(); + std::string blink_target; + + if (!params->getQueryParameter("blink_target", blink_target)) { + // if no blink_target was supplied in the URL parameters of the + // POST request, then assume we should blink the LED + blink_target = "LED"; + } + + if (blink_target == "LED") { + uint8_t count = 10; + while (count > 0) { + setLed(true); + delay(50); + setLed(false); + delay(50); + count = count - 1; + } + } else { + screen->blink(); + } + + res->println("{"); + res->println("\"status\": \"ok\""); + res->println("}"); +} + +void handleScanNetworks(HTTPRequest *req, HTTPResponse *res) +{ + res->setHeader("Content-Type", "application/json"); + res->setHeader("Access-Control-Allow-Origin", "*"); + res->setHeader("Access-Control-Allow-Methods", "GET"); + // res->setHeader("Content-Type", "text/html"); + + int n = WiFi.scanNetworks(); + res->println("{"); + res->println("\"data\": {"); + if (n == 0) { + // No networks found. + res->println("\"networks\": []"); + + } else { + res->println("\"networks\": ["); + + for (int i = 0; i < n; ++i) { + char ssidArray[50]; + String ssidString = String(WiFi.SSID(i)); + // String ssidString = String(WiFi.SSID(i)).toCharArray(ssidArray, WiFi.SSID(i).length()); + ssidString.replace("\"", "\\\""); + ssidString.toCharArray(ssidArray, 50); + + if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) { + // res->println("{\"ssid\": \"%s\",\"rssi\": -75}, ", String(WiFi.SSID(i).c_str() ); + + res->printf("{\"ssid\": \"%s\",\"rssi\": %d}", ssidArray, WiFi.RSSI(i)); + // WiFi.RSSI(i) + if (i != n - 1) { + res->printf(","); + } + } + // Yield some cpu cycles to IP stack. + // This is important in case the list is large and it takes us time to return + // to the main loop. + yield(); + } + res->println("]"); + } + res->println("},"); + res->println("\"status\": \"ok\""); + res->println("}"); +} diff --git a/src/mesh/http/ContentHandler.h b/src/mesh/http/ContentHandler.h new file mode 100644 index 00000000..8e22faef --- /dev/null +++ b/src/mesh/http/ContentHandler.h @@ -0,0 +1,46 @@ +#pragma once + +void registerHandlers(HTTPServer *insecureServer, HTTPSServer *secureServer); + +// Declare some handler functions for the various URLs on the server +void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res); +void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res); +void handleStyleCSS(HTTPRequest *req, HTTPResponse *res); +void handleHotspot(HTTPRequest *req, HTTPResponse *res); +void handleRoot(HTTPRequest *req, HTTPResponse *res); +void handleStaticBrowse(HTTPRequest *req, HTTPResponse *res); +void handleStaticPost(HTTPRequest *req, HTTPResponse *res); +void handleStatic(HTTPRequest *req, HTTPResponse *res); +void handleRestart(HTTPRequest *req, HTTPResponse *res); +void handle404(HTTPRequest *req, HTTPResponse *res); +void handleFormUpload(HTTPRequest *req, HTTPResponse *res); +void handleScanNetworks(HTTPRequest *req, HTTPResponse *res); +void handleSpiffsBrowseStatic(HTTPRequest *req, HTTPResponse *res); +void handleSpiffsDeleteStatic(HTTPRequest *req, HTTPResponse *res); +void handleBlinkLED(HTTPRequest *req, HTTPResponse *res); +void handleReport(HTTPRequest *req, HTTPResponse *res); +void handleFavicon(HTTPRequest *req, HTTPResponse *res); + +void middlewareSpeedUp240(HTTPRequest *req, HTTPResponse *res, std::function next); +void middlewareSpeedUp160(HTTPRequest *req, HTTPResponse *res, std::function next); +void middlewareSession(HTTPRequest *req, HTTPResponse *res, std::function next); + +uint32_t getTimeSpeedUp(); +void setTimeSpeedUp(); + + +// Interface to the PhoneAPI to access the protobufs with messages +class HttpAPI : public PhoneAPI +{ + + public: + // Nothing here yet + + private: + // Nothing here yet + + protected: + // Nothing here yet +}; + + diff --git a/src/mesh/http/ContentHelper.h b/src/mesh/http/ContentHelper.h index f94b9816..ca3fee3c 100644 --- a/src/mesh/http/ContentHelper.h +++ b/src/mesh/http/ContentHelper.h @@ -1,7 +1,7 @@ #include #include - +#define BoolToString(x) ((x) ? "true" : "false") void replaceAll(std::string &str, const std::string &from, const std::string &to); diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 193508b7..2633de52 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -1,16 +1,11 @@ #include "mesh/http/WebServer.h" #include "NodeDB.h" -#include "PowerFSM.h" -#include "airtime.h" -#include "main.h" -#include "mesh/http/ContentHelper.h" -#include "mesh/http/ContentStatic.h" #include "mesh/http/WiFiAPClient.h" -#include "sleep.h" #include #include #include -#include + + #include #include @@ -18,6 +13,7 @@ #include "esp_task_wdt.h" #endif + // Persistant Data Storage #include Preferences prefs; @@ -44,52 +40,18 @@ Preferences prefs; // The HTTPS Server comes in a separate namespace. For easier use, include it here. using namespace httpsserver; +#include "mesh/http/ContentHandler.h" SSLCert *cert; HTTPSServer *secureServer; HTTPServer *insecureServer; -// Our API to handle messages to and from the radio. -HttpAPI webAPI; -// Declare some handler functions for the various URLs on the server -void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res); -void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res); -void handleStyleCSS(HTTPRequest *req, HTTPResponse *res); -void handleHotspot(HTTPRequest *req, HTTPResponse *res); -void handleFavicon(HTTPRequest *req, HTTPResponse *res); -void handleRoot(HTTPRequest *req, HTTPResponse *res); -void handleStaticBrowse(HTTPRequest *req, HTTPResponse *res); -void handleStaticPost(HTTPRequest *req, HTTPResponse *res); -void handleStatic(HTTPRequest *req, HTTPResponse *res); -void handleRestart(HTTPRequest *req, HTTPResponse *res); -void handle404(HTTPRequest *req, HTTPResponse *res); -void handleFormUpload(HTTPRequest *req, HTTPResponse *res); -void handleScanNetworks(HTTPRequest *req, HTTPResponse *res); -void handleSpiffsBrowseStatic(HTTPRequest *req, HTTPResponse *res); -void handleSpiffsDeleteStatic(HTTPRequest *req, HTTPResponse *res); -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res); -void handleReport(HTTPRequest *req, HTTPResponse *res); -void middlewareSpeedUp240(HTTPRequest *req, HTTPResponse *res, std::function next); -void middlewareSpeedUp160(HTTPRequest *req, HTTPResponse *res, std::function next); -void middlewareSession(HTTPRequest *req, HTTPResponse *res, std::function next); bool isWebServerReady = 0; bool isCertReady = 0; -uint32_t timeSpeedUp = 0; - -uint32_t numberOfRequests = 0; - -// 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] = {{".txt", "text/plain"}, {".html", "text/html"}, - {".js", "text/javascript"}, {".png", "image/png"}, - {".jpg", "image/jpg"}, {".gz", "application/gzip"}, - {".gif", "image/gif"}, {".json", "application/json"}, - {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, - {".svg", "image/svg+xml"}, {"", ""}}; void handleWebResponse() { @@ -110,9 +72,10 @@ void handleWebResponse() Slow down the CPU if we have not received a request within the last few seconds. */ - if (millis() - timeSpeedUp >= (25 * 1000)) { + + if (millis() - getTimeSpeedUp() >= (25 * 1000)) { setCpuFrequencyMhz(80); - timeSpeedUp = millis(); + setTimeSpeedUp(); } } @@ -253,69 +216,7 @@ void initWebServer() secureServer = new HTTPSServer(cert); insecureServer = new HTTPServer(); - // For every resource available on the server, we need to create a ResourceNode - // The ResourceNode links URL and HTTP method to a handler function - - ResourceNode *nodeAPIv1ToRadioOptions = new ResourceNode("/api/v1/toradio", "OPTIONS", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1ToRadio = new ResourceNode("/api/v1/toradio", "PUT", &handleAPIv1ToRadio); - ResourceNode *nodeAPIv1FromRadio = new ResourceNode("/api/v1/fromradio", "GET", &handleAPIv1FromRadio); - - ResourceNode *nodeHotspot = new ResourceNode("/hotspot-detect.html", "GET", &handleHotspot); - ResourceNode *nodeFavicon = new ResourceNode("/favicon.ico", "GET", &handleFavicon); - ResourceNode *nodeRoot = new ResourceNode("/", "GET", &handleRoot); - ResourceNode *nodeStaticBrowse = new ResourceNode("/static", "GET", &handleStaticBrowse); - ResourceNode *nodeStaticPOST = new ResourceNode("/static", "POST", &handleStaticPost); - ResourceNode *nodeStatic = new ResourceNode("/static/*", "GET", &handleStatic); - ResourceNode *nodeRestart = new ResourceNode("/restart", "POST", &handleRestart); - ResourceNode *node404 = new ResourceNode("", "GET", &handle404); - ResourceNode *nodeFormUpload = new ResourceNode("/upload", "POST", &handleFormUpload); - ResourceNode *nodeJsonScanNetworks = new ResourceNode("/json/scanNetworks", "GET", &handleScanNetworks); - ResourceNode *nodeJsonBlinkLED = new ResourceNode("/json/blink", "POST", &handleBlinkLED); - ResourceNode *nodeJsonReport = new ResourceNode("/json/report", "GET", &handleReport); - ResourceNode *nodeJsonSpiffsBrowseStatic = new ResourceNode("/json/spiffs/browse/static/", "GET", &handleSpiffsBrowseStatic); - ResourceNode *nodeJsonDelete = new ResourceNode("/json/spiffs/delete/static", "DELETE", &handleSpiffsDeleteStatic); - - // Secure nodes - secureServer->registerNode(nodeAPIv1ToRadioOptions); - secureServer->registerNode(nodeAPIv1ToRadio); - secureServer->registerNode(nodeAPIv1FromRadio); - secureServer->registerNode(nodeHotspot); - secureServer->registerNode(nodeFavicon); - secureServer->registerNode(nodeRoot); - secureServer->registerNode(nodeStaticBrowse); - secureServer->registerNode(nodeStaticPOST); - secureServer->registerNode(nodeStatic); - secureServer->registerNode(nodeRestart); - secureServer->registerNode(nodeFormUpload); - secureServer->registerNode(nodeJsonScanNetworks); - secureServer->registerNode(nodeJsonBlinkLED); - secureServer->registerNode(nodeJsonSpiffsBrowseStatic); - secureServer->registerNode(nodeJsonDelete); - secureServer->registerNode(nodeJsonReport); - secureServer->setDefaultNode(node404); - - secureServer->addMiddleware(&middlewareSpeedUp240); - - // Insecure nodes - insecureServer->registerNode(nodeAPIv1ToRadioOptions); - insecureServer->registerNode(nodeAPIv1ToRadio); - insecureServer->registerNode(nodeAPIv1FromRadio); - insecureServer->registerNode(nodeHotspot); - insecureServer->registerNode(nodeFavicon); - insecureServer->registerNode(nodeRoot); - insecureServer->registerNode(nodeStaticBrowse); - insecureServer->registerNode(nodeStaticPOST); - insecureServer->registerNode(nodeStatic); - insecureServer->registerNode(nodeRestart); - insecureServer->registerNode(nodeFormUpload); - insecureServer->registerNode(nodeJsonScanNetworks); - insecureServer->registerNode(nodeJsonBlinkLED); - insecureServer->registerNode(nodeJsonSpiffsBrowseStatic); - insecureServer->registerNode(nodeJsonDelete); - insecureServer->registerNode(nodeJsonReport); - insecureServer->setDefaultNode(node404); - - insecureServer->addMiddleware(&middlewareSpeedUp160); + registerHandlers(insecureServer, secureServer); DEBUG_MSG("Starting Web Servers...\n"); secureServer->start(); @@ -328,926 +229,3 @@ void initWebServer() } } -void middlewareSpeedUp240(HTTPRequest *req, HTTPResponse *res, std::function next) -{ - // We want to print the response status, so we need to call next() first. - next(); - - // Phone (or other device) has contacted us over WiFi. Keep the radio turned on. - // TODO: This should go into its own middleware layer separate from the speedup. - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); - - setCpuFrequencyMhz(240); - timeSpeedUp = millis(); - - numberOfRequests++; -} - -void middlewareSpeedUp160(HTTPRequest *req, HTTPResponse *res, std::function next) -{ - // We want to print the response status, so we need to call next() first. - next(); - - // Phone (or other device) has contacted us over WiFi. Keep the radio turned on. - // TODO: This should go into its own middleware layer separate from the speedup. - powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); - - // If the frequency is 240mhz, we have recently gotten a HTTPS request. - // In that case, leave the frequency where it is and just update the - // countdown timer (timeSpeedUp). - if (getCpuFrequencyMhz() != 240) { - setCpuFrequencyMhz(160); - } - timeSpeedUp = millis(); - - numberOfRequests++; -} - -void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) -{ - - DEBUG_MSG("+++++++++++++++ webAPI handleAPIv1FromRadio\n"); - - /* - For documentation, see: - https://github.com/meshtastic/Meshtastic-device/wiki/HTTP-REST-API-discussion - https://github.com/meshtastic/Meshtastic-device/blob/master/docs/software/device-api.md - - Example: - http://10.10.30.198/api/v1/fromradio - */ - - // Get access to the parameters - ResourceParameters *params = req->getParams(); - - // std::string paramAll = "all"; - std::string valueAll; - - // Status code is 200 OK by default. - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "PUT, GET"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/Meshtastic-protobufs/master/mesh.proto"); - - uint8_t txBuf[MAX_STREAM_BUF_SIZE]; - uint32_t len = 1; - - if (params->getQueryParameter("all", valueAll)) { - - // If all is ture, return all the buffers we have available - // to us at this point in time. - if (valueAll == "true") { - while (len) { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - // Otherwise, just return one protobuf - } else { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - // the param "all" was not spcified. Return just one protobuf - } else { - len = webAPI.getFromRadio(txBuf); - res->write(txBuf, len); - } - - DEBUG_MSG("--------------- webAPI handleAPIv1FromRadio, len %d\n", len); -} - -void handleAPIv1ToRadio(HTTPRequest *req, HTTPResponse *res) -{ - DEBUG_MSG("+++++++++++++++ webAPI handleAPIv1ToRadio\n"); - - /* - For documentation, see: - https://github.com/meshtastic/Meshtastic-device/wiki/HTTP-REST-API-discussion - https://github.com/meshtastic/Meshtastic-device/blob/master/docs/software/device-api.md - - Example: - http://10.10.30.198/api/v1/toradio - */ - - // Status code is 200 OK by default. - - res->setHeader("Content-Type", "application/x-protobuf"); - res->setHeader("Access-Control-Allow-Headers", "Content-Type"); - res->setHeader("Access-Control-Allow-Origin", "*"); - res->setHeader("Access-Control-Allow-Methods", "PUT, OPTIONS"); - res->setHeader("X-Protobuf-Schema", "https://raw.githubusercontent.com/meshtastic/Meshtastic-protobufs/master/mesh.proto"); - - if (req->getMethod() == "OPTIONS") { - res->setStatusCode(204); // Success with no content - res->print(""); - return; - } - - byte buffer[MAX_TO_FROM_RADIO_SIZE]; - size_t s = req->readBytes(buffer, MAX_TO_FROM_RADIO_SIZE); - - DEBUG_MSG("Received %d bytes from PUT request\n", s); - webAPI.handleToRadio(buffer, s); - - res->write(buffer, s); - DEBUG_MSG("--------------- webAPI handleAPIv1ToRadio\n"); -} - -void handleStaticPost(HTTPRequest *req, HTTPResponse *res) -{ - // Assume POST request. Contains submitted data. - res->println("File Edited

File Edited

"); - - // The form is submitted with the x-www-form-urlencoded content type, so we need the - // HTTPURLEncodedBodyParser to read the fields. - // Note that the content of the file's content comes from a
"); - res->println(""); - res->println(""); - res->println(""); - - return; - } - - res->println("

Upload new file

"); - res->println("

This form allows you to upload files. Keep your filenames small and files under 200k.

"); - res->println("
"); - res->println("file:
"); - res->println(""); - res->println("
"); - - res->println("

All Files

"); - - File root = SPIFFS.open("/"); - if (root.isDirectory()) { - res->println(""); - - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - - File file = root.openNextFile(); - while (file) { - String filePath = String(file.name()); - if (filePath.indexOf("/static") == 0) { - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - res->println(""); - } - - file = root.openNextFile(); - } - res->println("
File"); - res->println("Size"); - res->println("Actions"); - res->println("
"); - - if (String(file.name()).substring(1).endsWith(".gz")) { - String modifiedFile = String(file.name()).substring(1); - modifiedFile.remove((modifiedFile.length() - 3), 3); - res->print("" + String(file.name()).substring(1) + ""); - } else { - res->print("" + String(file.name()).substring(1) + - ""); - } - res->println(""); - res->print(String(file.size())); - res->println(""); - res->print("Delete "); - res->println(""); - if (!String(file.name()).substring(1).endsWith(".gz")) { - res->print("Edit"); - } - res->println("
"); - - res->print("
"); - // res->print("Total : " + String(SPIFFS.totalBytes()) + " Bytes
"); - res->print("Used : " + String(SPIFFS.usedBytes()) + " Bytes
"); - res->print("Free : " + String(SPIFFS.totalBytes() - SPIFFS.usedBytes()) + " Bytes
"); - } -} - -void handleStatic(HTTPRequest *req, HTTPResponse *res) -{ - // Get access to the parameters - ResourceParameters *params = req->getParams(); - - std::string parameter1; - // Print the first parameter value - if (params->getPathParameter(0, parameter1)) { - - std::string filename = "/static/" + parameter1; - std::string filenameGzip = "/static/" + parameter1 + ".gz"; - - if (!SPIFFS.exists(filename.c_str()) && !SPIFFS.exists(filenameGzip.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"); - res->printf("

File not found: %s

\n", filename.c_str()); - return; - } - - // Try to open the file from SPIFFS - File file; - - if (SPIFFS.exists(filename.c_str())) { - file = SPIFFS.open(filename.c_str()); - if (!file.available()) { - DEBUG_MSG("File not available - %s\n", filename.c_str()); - } - - } else if (SPIFFS.exists(filenameGzip.c_str())) { - file = SPIFFS.open(filenameGzip.c_str()); - res->setHeader("Content-Encoding", "gzip"); - if (!file.available()) { - DEBUG_MSG("File not available\n"); - } - } - - res->setHeader("Content-Length", httpsserver::intToString(file.size())); - - bool has_set_content_type = false; - // Content-Type is guessed using the definition of the contentTypes-table defined above - int cTypeIdx = 0; - do { - if (filename.rfind(contentTypes[cTypeIdx][0]) != std::string::npos) { - res->setHeader("Content-Type", contentTypes[cTypeIdx][1]); - has_set_content_type = true; - break; - } - cTypeIdx += 1; - } while (strlen(contentTypes[cTypeIdx][0]) > 0); - - if (!has_set_content_type) { - // Set a default content type - res->setHeader("Content-Type", "application/octet-stream"); - } - - // Read the file from SPIFFS and write it to the HTTP response body - size_t length = 0; - do { - char buffer[256]; - length = file.read((uint8_t *)buffer, 256); - std::string bufferString(buffer, length); - res->write((uint8_t *)bufferString.c_str(), bufferString.size()); - } while (length > 0); - - file.close(); - - return; - - } else { - res->println("ERROR: This should not have happened..."); - } -} - -void handleFormUpload(HTTPRequest *req, HTTPResponse *res) -{ - - DEBUG_MSG("Form Upload - Disabling keep-alive\n"); - res->setHeader("Connection", "close"); - - DEBUG_MSG("Form Upload - Set frequency to 240mhz\n"); - // The upload process is very CPU intensive. Let's speed things up a bit. - setCpuFrequencyMhz(240); - - // First, we need to check the encoding of the form that we have received. - // The browser will set the Content-Type request header, so we can use it for that purpose. - // Then we select the body parser based on the encoding. - // Actually we do this only for documentary purposes, we know the form is going - // to be multipart/form-data. - DEBUG_MSG("Form Upload - Creating body parser reference\n"); - HTTPBodyParser *parser; - std::string contentType = req->getHeader("Content-Type"); - - // The content type may have additional properties after a semicolon, for exampel: - // Content-Type: text/html;charset=utf-8 - // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs - // As we're interested only in the actual mime _type_, we strip everything after the - // first semicolon, if one exists: - size_t semicolonPos = contentType.find(";"); - if (semicolonPos != std::string::npos) { - contentType = contentType.substr(0, semicolonPos); - } - - // Now, we can decide based on the content type: - if (contentType == "multipart/form-data") { - DEBUG_MSG("Form Upload - multipart/form-data\n"); - parser = new HTTPMultipartBodyParser(req); - } else { - Serial.printf("Unknown POST Content-Type: %s\n", contentType.c_str()); - return; - } - - res->println("File " - "Upload

File Upload

"); - - // We iterate over the fields. Any field with a filename is uploaded. - // Note that the BodyParser consumes the request body, meaning that you can iterate over the request's - // fields only a single time. The reason for this is that it allows you to handle large requests - // which would not fit into memory. - bool didwrite = false; - - // parser->nextField() will move the parser to the next field in the request body (field meaning a - // form field, if you take the HTML perspective). After the last field has been processed, nextField() - // returns false and the while loop ends. - while (parser->nextField()) { - // For Multipart data, each field has three properties: - // The name ("name" value of the tag) - // The filename (If it was a , this is the filename on the machine of the - // user uploading it) - // The mime type (It is determined by the client. So do not trust this value and blindly start - // parsing files only if the type matches) - std::string name = parser->getFieldName(); - std::string filename = parser->getFieldFilename(); - std::string mimeType = parser->getFieldMimeType(); - // We log all three values, so that you can observe the upload on the serial monitor: - DEBUG_MSG("handleFormUpload: field name='%s', filename='%s', mimetype='%s'\n", name.c_str(), filename.c_str(), - mimeType.c_str()); - - // Double check that it is what we expect - if (name != "file") { - DEBUG_MSG("Skipping unexpected field\n"); - res->println("

No file found.

"); - return; - } - - // Double check that it is what we expect - if (filename == "") { - DEBUG_MSG("Skipping unexpected field\n"); - res->println("

No file found.

"); - return; - } - - // SPIFFS limits the total lenth of a path + file to 31 characters. - if (filename.length() + 8 > 31) { - DEBUG_MSG("Uploaded filename too long!\n"); - res->println("

Uploaded filename too long! Limit of 23 characters.

"); - delete parser; - return; - } - - // You should check file name validity and all that, but we skip that to make the core - // concepts of the body parser functionality easier to understand. - std::string pathname = "/static/" + filename; - - // Create a new file on spiffs to stream the data into - File file = SPIFFS.open(pathname.c_str(), "w"); - size_t fileLength = 0; - didwrite = true; - - // With endOfField you can check whether the end of field has been reached or if there's - // still data pending. With multipart bodies, you cannot know the field size in advance. - while (!parser->endOfField()) { - esp_task_wdt_reset(); - - byte buf[512]; - size_t readLength = parser->read(buf, 512); - // DEBUG_MSG("\n\nreadLength - %i\n", readLength); - - // Abort the transfer if there is less than 50k space left on the filesystem. - if (SPIFFS.totalBytes() - SPIFFS.usedBytes() < 51200) { - file.close(); - res->println("

Write aborted! Reserving 50k on filesystem.

"); - - // enableLoopWDT(); - - delete parser; - return; - } - - // if (readLength) { - file.write(buf, readLength); - fileLength += readLength; - DEBUG_MSG("File Length %i\n", fileLength); - //} - } - // enableLoopWDT(); - - file.close(); - res->printf("

Saved %d bytes to %s

", (int)fileLength, pathname.c_str()); - } - if (!didwrite) { - res->println("

Did not write any file

"); - } - res->println(""); - delete parser; -} - -void handle404(HTTPRequest *req, HTTPResponse *res) -{ - - // Discard request body, if we received any - // We do this, as this is the default node and may also server POST/PUT requests - req->discardRequestBody(); - - // Set the response status - res->setStatusCode(404); - res->setStatusText("Not Found"); - - // Set content type of the response - res->setHeader("Content-Type", "text/html"); - - // Write a tiny HTTP page - res->println(""); - res->println(""); - res->println("Not Found"); - res->println("

404 Not Found

The requested resource was not found on this server.

"); - res->println(""); -} - -/* - This supports the Apple Captive Network Assistant (CNA) Portal -*/ -void handleHotspot(HTTPRequest *req, HTTPResponse *res) -{ - DEBUG_MSG("Hotspot Request\n"); - - /* - If we don't do a redirect, be sure to return a "Success" message - otherwise iOS will have trouble detecting that the connection to the SoftAP worked. - */ - - // Status code is 200 OK by default. - // We want to deliver a simple HTML page, so we send a corresponding content type: - res->setHeader("Content-Type", "text/html"); - - // res->println(""); - res->println("\n"); -} - -/* - To convert text to c strings: - - https://tomeko.net/online_tools/cpp_text_escape.php?lang=en -*/ -void handleRoot(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - - res->setHeader("Set-Cookie", - "mt_session=" + httpsserver::intToString(random(1, 9999999)) + "; Expires=Wed, 20 Apr 2049 4:20:00 PST"); - - std::string cookie = req->getHeader("Cookie"); - // String cookieString = cookie.c_str(); - // uint8_t nameIndex = cookieString.indexOf("mt_session"); - // DEBUG_MSG(cookie.c_str()); - - std::string filename = "/static/index.html"; - std::string filenameGzip = "/static/index.html.gz"; - - if (!SPIFFS.exists(filename.c_str()) && !SPIFFS.exists(filenameGzip.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"); - res->printf("

File not found: %s

\n", filename.c_str()); - res->printf("

\n"); - res->printf("

You have gotten this error because the filesystem for the web server has not been loaded.

\n"); - res->printf("

Please review the 'Common Problems' section of the web interface documentation.

\n"); - return; - } - - // Try to open the file from SPIFFS - File file; - - if (SPIFFS.exists(filename.c_str())) { - file = SPIFFS.open(filename.c_str()); - if (!file.available()) { - DEBUG_MSG("File not available - %s\n", filename.c_str()); - } - - } else if (SPIFFS.exists(filenameGzip.c_str())) { - file = SPIFFS.open(filenameGzip.c_str()); - res->setHeader("Content-Encoding", "gzip"); - if (!file.available()) { - DEBUG_MSG("File not available\n"); - } - } - - // Read the file from SPIFFS and write it to the HTTP response body - size_t length = 0; - do { - char buffer[256]; - length = file.read((uint8_t *)buffer, 256); - std::string bufferString(buffer, length); - res->write((uint8_t *)bufferString.c_str(), bufferString.size()); - } while (length > 0); -} - -void handleRestart(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "text/html"); - - DEBUG_MSG("***** Restarted on HTTP(s) Request *****\n"); - res->println("Restarting"); - - ESP.restart(); -} - -void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) -{ - res->setHeader("Content-Type", "application/json"); - - ResourceParameters *params = req->getParams(); - std::string blink_target; - - if (!params->getQueryParameter("blink_target", blink_target)) { - // if no blink_target was supplied in the URL parameters of the - // POST request, then assume we should blink the LED - blink_target = "LED"; - } - - if (blink_target == "LED") { - uint8_t count = 10; - while (count > 0) { - setLed(true); - delay(50); - setLed(false); - delay(50); - count = count - 1; - } - } else { - screen->blink(); - } - - res->println("{"); - res->println("\"status\": \"ok\""); - res->println("}"); -} - -void handleReport(HTTPRequest *req, HTTPResponse *res) -{ - - ResourceParameters *params = req->getParams(); - std::string content; - - if (!params->getQueryParameter("content", content)) { - content = "json"; - } - - if (content == "json") { - res->setHeader("Content-Type", "application/json"); - - } else { - res->setHeader("Content-Type", "text/html"); - res->println("
");
-    }
-
-    res->println("{");
-
-    res->println("\"data\": {");
-
-    res->println("\"airtime\": {");
-
-    uint32_t *logArray;
-
-    res->print("\"tx_log\": [");
-
-    logArray = airtimeReport(TX_LOG);
-    for (int i = 0; i < getPeriodsToLog(); i++) {
-        uint32_t tmp;
-        tmp = *(logArray + i);
-        res->printf("%d", tmp);
-        if (i != getPeriodsToLog() - 1) {
-            res->print(", ");
-        }
-    }
-
-    res->println("],");
-    res->print("\"rx_log\": [");
-
-    logArray = airtimeReport(RX_LOG);
-    for (int i = 0; i < getPeriodsToLog(); i++) {
-        uint32_t tmp;
-        tmp = *(logArray + i);
-        res->printf("%d", tmp);
-        if (i != getPeriodsToLog() - 1) {
-            res->print(", ");
-        }
-    }
-
-    res->println("],");
-    res->print("\"rx_all_log\": [");
-
-    logArray = airtimeReport(RX_ALL_LOG);
-    for (int i = 0; i < getPeriodsToLog(); i++) {
-        uint32_t tmp;
-        tmp = *(logArray + i);
-        res->printf("%d", tmp);
-        if (i != getPeriodsToLog() - 1) {
-            res->print(", ");
-        }
-    }
-
-    res->println("],");
-    res->printf("\"seconds_since_boot\": %u,\n", getSecondsSinceBoot());
-    res->printf("\"seconds_per_period\": %u,\n", getSecondsPerPeriod());
-    res->printf("\"periods_to_log\": %u\n", getPeriodsToLog());
-
-    res->println("},");
-
-    res->println("\"wifi\": {");
-
-    res->printf("\"web_request_count\": %d,\n", numberOfRequests);
-    res->println("\"rssi\": " + String(WiFi.RSSI()) + ",");
-
-    if (radioConfig.preferences.wifi_ap_mode || isSoftAPForced()) {
-        res->println("\"ip\": \"" + String(WiFi.softAPIP().toString().c_str()) + "\"");
-    } else {
-        res->println("\"ip\": \"" + String(WiFi.localIP().toString().c_str()) + "\"");
-    }
-
-    res->println("},");
-
-    res->println("\"memory\": {");
-    res->printf("\"heap_total\": %d,\n", ESP.getHeapSize());
-    res->printf("\"heap_free\": %d,\n", ESP.getFreeHeap());
-    res->printf("\"psram_total\": %d,\n", ESP.getPsramSize());
-    res->printf("\"psram_free\": %d,\n", ESP.getFreePsram());
-    res->println("\"spiffs_total\" : " + String(SPIFFS.totalBytes()) + ",");
-    res->println("\"spiffs_used\" : " + String(SPIFFS.usedBytes()) + ",");
-    res->println("\"spiffs_free\" : " + String(SPIFFS.totalBytes() - SPIFFS.usedBytes()));
-    res->println("},");
-
-    res->println("\"power\": {");
-#define BoolToString(x) ((x) ? "true" : "false")
-    res->printf("\"battery_percent\": %u,\n", powerStatus->getBatteryChargePercent());
-    res->printf("\"battery_voltage_mv\": %u,\n", powerStatus->getBatteryVoltageMv());
-    res->printf("\"has_battery\": %s,\n", BoolToString(powerStatus->getHasBattery()));
-    res->printf("\"has_usb\": %s,\n", BoolToString(powerStatus->getHasUSB()));
-    res->printf("\"is_charging\": %s\n", BoolToString(powerStatus->getIsCharging()));
-    res->println("}");
-
-    res->println("},");
-
-    res->println("\"status\": \"ok\"");
-    res->println("}");
-}
-
-void handleScanNetworks(HTTPRequest *req, HTTPResponse *res)
-{
-    res->setHeader("Content-Type", "application/json");
-    // res->setHeader("Content-Type", "text/html");
-
-    int n = WiFi.scanNetworks();
-    res->println("{");
-    res->println("\"data\": {");
-    if (n == 0) {
-        // No networks found.
-        res->println("\"networks\": []");
-
-    } else {
-        res->println("\"networks\": [");
-
-        for (int i = 0; i < n; ++i) {
-            char ssidArray[50];
-            String ssidString = String(WiFi.SSID(i));
-            // String ssidString = String(WiFi.SSID(i)).toCharArray(ssidArray, WiFi.SSID(i).length());
-            ssidString.replace("\"", "\\\"");
-            ssidString.toCharArray(ssidArray, 50);
-
-            if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {
-                // res->println("{\"ssid\": \"%s\",\"rssi\": -75}, ", String(WiFi.SSID(i).c_str() );
-
-                res->printf("{\"ssid\": \"%s\",\"rssi\": %d}", ssidArray, WiFi.RSSI(i));
-                // WiFi.RSSI(i)
-                if (i != n - 1) {
-                    res->printf(",");
-                }
-            }
-            // Yield some cpu cycles to IP stack.
-            //   This is important in case the list is large and it takes us time to return
-            //   to the main loop.
-            yield();
-        }
-        res->println("]");
-    }
-    res->println("},");
-    res->println("\"status\": \"ok\"");
-    res->println("}");
-}
-
-void handleFavicon(HTTPRequest *req, HTTPResponse *res)
-{
-    // Set Content-Type
-    res->setHeader("Content-Type", "image/vnd.microsoft.icon");
-    // Write data from header file
-    res->write(FAVICON_DATA, FAVICON_LENGTH);
-}
diff --git a/src/mesh/http/WebServer.h b/src/mesh/http/WebServer.h
index b01d5960..aacb2e00 100644
--- a/src/mesh/http/WebServer.h
+++ b/src/mesh/http/WebServer.h
@@ -8,31 +8,10 @@
 void initWebServer();
 void createSSLCert();
 
-void handleNotFound();
 
 void handleWebResponse();
 
 
-//void handleHotspot();
-
-//void handleStyleCSS();
-//void handleRoot();
-
-
-// Interface to the PhoneAPI to access the protobufs with messages
-class HttpAPI : public PhoneAPI
-{
-
-  public:
-    // Nothing here yet
-
-  private:
-    // Nothing here yet
-
-  protected:
-    // Nothing here yet
-};
-
 class WebServerThread : private concurrency::OSThread
 {
 
diff --git a/src/plugins/SerialPlugin.cpp b/src/plugins/SerialPlugin.cpp
index 7e764ee8..47f27908 100644
--- a/src/plugins/SerialPlugin.cpp
+++ b/src/plugins/SerialPlugin.cpp
@@ -10,7 +10,7 @@
 
 /*
     SerialPlugin
-        An overly simplistic interface to send messages over the mesh network by sending strings
+        A simple interface to send messages over the mesh network by sending strings
         over a serial port.
 
         Default is to use RX GPIO 16 and TX GPIO 17.
@@ -20,23 +20,21 @@
 
     Basic Usage:
 
-        1) Enable the plugin by setting SERIALPLUGIN_ENABLED to 1.
-        2) Set the pins (RXD2 / TXD2) for your preferred RX and TX GPIO pins.
+        1) Enable the plugin by setting serialplugin_enabled to 1.
+        2) Set the pins (serialplugin_rxd / serialplugin_rxd) for your preferred RX and TX GPIO pins.
            On tbeam, recommend to use:
-                #define RXD2 35
-                #define TXD2 15
-        3) Set SERIALPLUGIN_TIMEOUT to the amount of time to wait before we consider
+                RXD 35
+                TXD 15
+        3) Set serialplugin_timeout to the amount of time to wait before we consider
            your packet as "done".
         4) (Optional) In SerialPlugin.h set the port to PortNum_TEXT_MESSAGE_APP if you want to
            send messages to/from the general text message channel.
         5) Connect to your device over the serial interface at 38400 8N1.
         6) Send a packet up to 240 bytes in length. This will get relayed over the mesh network.
-        7) (Optional) Set SERIALPLUGIN_ECHO to 1 and any message you send out will be echoed back
+        7) (Optional) Set serialplugin_echo to 1 and any message you send out will be echoed back
            to your device.
 
     TODO (in this order):
-        * Once protobufs regenerated with the new port, update SerialPlugin.h
-        * Ensure this works on a tbeam
         * Define a verbose RX mode to report on mesh and packet infomration.
             - This won't happen any time soon.
 
@@ -76,13 +74,14 @@ int32_t SerialPlugin::runOnce()
     // radioConfig.preferences.serialplugin_rxd = 35;
     // radioConfig.preferences.serialplugin_txd = 15;
     // radioConfig.preferences.serialplugin_timeout = 1000;
+    // radioConfig.preferences.serialplugin_echo = 1;
 
     if (radioConfig.preferences.serialplugin_enabled) {
 
         if (firstTime) {
 
             // Interface with the serial peripheral from in here.
-            DEBUG_MSG("Initilizing serial peripheral interface\n");
+            DEBUG_MSG("Initializing serial peripheral interface\n");
 
             if (radioConfig.preferences.serialplugin_rxd && radioConfig.preferences.serialplugin_txd) {
                 Serial2.begin(SERIALPLUGIN_BAUD, SERIAL_8N1, radioConfig.preferences.serialplugin_rxd,
diff --git a/version.properties b/version.properties
index ac4a0363..556f3f84 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
 [VERSION]  
 major = 1  
 minor = 1
-build = 32
+build = 33