From fc469f47a6add27870ddb26502242af943e38fd0 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 20 Jun 2020 19:28:34 +0200 Subject: [PATCH] Switched to native ESP32 webserver - new status API - webdav without crashing --- firmware/lib/basic/FilesystemAbstraction.h | 10 + firmware/lib/esphttp/EspHttp.cpp | 27 +++ firmware/lib/esphttp/EspHttp.h | 71 ++++++ firmware/lib/esphttp/WebDAV.cpp | 137 ++++++++++++ firmware/lib/esphttp/WebDAV.h | 4 + firmware/lib/session/MeasurementSession.h | 4 + firmware/src/ConfigHardware.h | 2 +- firmware/src/firmware_main.cpp | 241 +++++++++++---------- 8 files changed, 385 insertions(+), 111 deletions(-) create mode 100644 firmware/lib/esphttp/EspHttp.cpp create mode 100644 firmware/lib/esphttp/EspHttp.h create mode 100644 firmware/lib/esphttp/WebDAV.cpp create mode 100644 firmware/lib/esphttp/WebDAV.h diff --git a/firmware/lib/basic/FilesystemAbstraction.h b/firmware/lib/basic/FilesystemAbstraction.h index 790f5b8..6fc20b8 100644 --- a/firmware/lib/basic/FilesystemAbstraction.h +++ b/firmware/lib/basic/FilesystemAbstraction.h @@ -74,6 +74,16 @@ namespace portablefs { return SPIFFS.mkdir(name); } + + inline size_t usedBytes() + { + return SPIFFS.usedBytes(); + } + + inline size_t totalBytes() + { + return SPIFFS.totalBytes(); + } } // namespace portablefs diff --git a/firmware/lib/esphttp/EspHttp.cpp b/firmware/lib/esphttp/EspHttp.cpp new file mode 100644 index 0000000..d68d040 --- /dev/null +++ b/firmware/lib/esphttp/EspHttp.cpp @@ -0,0 +1,27 @@ +#include "EspHttp.h" + +esp_err_t rawCallback(httpd_req_t *req) +{ + EspHttp::Callback *userCb = (EspHttp::Callback *)(req->user_ctx); + (*userCb)(req); + return ESP_OK; +} + +int getUrlQueryParameter(httpd_req_t *req, const char *name, int defaultValue) +{ + size_t bufferLength = httpd_req_get_url_query_len(req) + 1; + int result = defaultValue; + if (bufferLength > 1) + { + char* buf = (char*)malloc(bufferLength); + if (httpd_req_get_url_query_str(req, buf, bufferLength) == ESP_OK) + { + char param[32]; + /* Get value of expected key from query string */ + if (httpd_query_key_value(buf, name, param, sizeof(param)) == ESP_OK) + result = atoi(param); + } + free(buf); + } + return result; +} diff --git a/firmware/lib/esphttp/EspHttp.h b/firmware/lib/esphttp/EspHttp.h new file mode 100644 index 0000000..20f50f1 --- /dev/null +++ b/firmware/lib/esphttp/EspHttp.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include "Dtypes.h" + +constexpr int MAX_URI_HANDLERS = 10; + +esp_err_t rawCallback(httpd_req_t *req); + +class EspHttp +{ +public: + using Callback = std::function; + + EspHttp(int port = 80) : server_(0), nextFreeHandler_(0) + { + config_ = HTTPD_DEFAULT_CONFIG(); + config_.server_port = port; + config_.max_uri_handlers = MAX_URI_HANDLERS; + config_.uri_match_fn = httpd_uri_match_wildcard; + } + ~EspHttp() + { + stop(); + } + + void start() + { + if (httpd_start(&server_, &config_) != ESP_OK) + assert_msg(false, "HTTPD server didn't start"); + } + + void stop() + { + if (server_) + httpd_stop(server_); + server_ = 0; + } + + void on(const char *uri, httpd_method_t method, Callback cb) + { + if (nextFreeHandler_ == MAX_URI_HANDLERS) + { + assert_msg(nextFreeHandler_ < MAX_URI_HANDLERS, "Too many handlers - increase MAX_URI_HANDLERS"); + return; + } + handlerCallbacks_[nextFreeHandler_] = cb; + + httpd_uri_t uri_endpoint_cfg = { + .uri = uri, + .method = method, + .handler = rawCallback, + .user_ctx = (void *)(&handlerCallbacks_[nextFreeHandler_])}; + if (httpd_register_uri_handler(server_, &uri_endpoint_cfg) != ESP_OK) + Serial.println("Failed to register url handler"); + ++nextFreeHandler_; + } + + void write(const char *data, ssize_t length); + +private: + httpd_config_t config_; + httpd_handle_t server_; + int nextFreeHandler_; + Callback handlerCallbacks_[MAX_URI_HANDLERS]; +}; + +esp_err_t rawCallback(httpd_req_t *req); + +int getUrlQueryParameter(httpd_req_t *req, const char *name, int defaultValue); \ No newline at end of file diff --git a/firmware/lib/esphttp/WebDAV.cpp b/firmware/lib/esphttp/WebDAV.cpp new file mode 100644 index 0000000..c1282dc --- /dev/null +++ b/firmware/lib/esphttp/WebDAV.cpp @@ -0,0 +1,137 @@ +#include "FilesystemAbstraction.h" +#include "WebDAV.h" + +namespace webdav_constants +{ + constexpr const char *MULTISTATUS_START = ""; + constexpr const char *MULTISTATUS_END = ""; + constexpr const char *RESPONSE_START = ""; + constexpr const char *RESPONSE_END = "\n"; + constexpr const char *HREF_START = ""; + constexpr const char *HREF_END = ""; + constexpr const char *PROPSTAT_START = ""; + constexpr const char *PROPSTAT_END = ""; + constexpr const char *PROP_START = ""; + constexpr const char *PROP_END = ""; + constexpr const char *RESOURCETYPE_START = ""; + constexpr const char *RESOURCETYPE_END = ""; + constexpr const char *RESOURCE_COLLECTION = ""; + constexpr const char *HTTP_204_NO_CONTENT = "HTTP/1.1 204 No Content"; + + constexpr const char *CONTENTLEN_START = ""; + constexpr const char *CONTENTLEN_END = ""; + constexpr const char *CREATEDATE_START = ""; + constexpr const char *CREATEDATE_END = ""; + constexpr const char *MODDATE_START = ""; + constexpr const char *MODDATE_END = ""; + constexpr const char *STATUS_OK = "HTTP/1.1 200 OK"; + constexpr const char *USED_BYTES_START = ""; + constexpr const char *USED_BYTES_END = ""; +} // namespace webdav_constants + +size_t webdavFileListingSpiffs(char *buffer, size_t maxLength, + const char *spiffsPath) +{ + using namespace webdav_constants; + + size_t bytesWritten = 0; + + auto toBuffer = [&buffer, &bytesWritten](const char *text) { + auto len = strlen(text); + memcpy(buffer, text, len); + buffer += len; + bytesWritten += len; + }; + + // crude upper bound on how much space a file entry + footer needs + constexpr size_t sizePerFile = 512; + assert(maxLength >= sizePerFile); + + int fileIdx = 0; + + auto dir = portablefs::openDir(spiffsPath); + + toBuffer(MULTISTATUS_START); + + bool incomplete = false; + while (dir.next()) + { + const auto freeSpace = maxLength - bytesWritten; + if (freeSpace < sizePerFile) + { + incomplete = true; + break; + } + fileIdx += 1; + toBuffer(RESPONSE_START); + toBuffer(HREF_START); + const auto fileName = dir.fileName(); + const auto fileNameWithoutDir = fileName.substring(fileName.lastIndexOf("/") + 1); + toBuffer((fileNameWithoutDir + ".st").c_str()); + toBuffer(HREF_END); + toBuffer(PROPSTAT_START); + toBuffer(PROP_START); + if (dir.isDirectory()) + { + toBuffer(RESOURCETYPE_START); + toBuffer(RESOURCE_COLLECTION); + toBuffer(RESOURCETYPE_END); + } + else + { + toBuffer(CONTENTLEN_START); + String fileSizeStr(dir.fileSize()); + toBuffer(fileSizeStr.c_str()); + toBuffer(CONTENTLEN_END); + } + + toBuffer(PROP_END); + toBuffer(STATUS_OK); + toBuffer(PROPSTAT_END); + toBuffer(webdav_constants::RESPONSE_END); + } + toBuffer(MULTISTATUS_END); + + if (incomplete) + Serial.println("WebDAV listing response is incomplete, because buffer was too small"); + return bytesWritten; +} + +String uriToFileName(const String &uriStr) +{ + String filename; + if (uriStr.endsWith(".st")) + filename = uriStr.substring(0, uriStr.length() - strlen(".st")); + + filename = "/dat/" + filename; + return filename; +} + +void webdavHandler(httpd_req_t *req) +{ + String uri = String(req->uri).substring(strlen("/webdav/")); + constexpr size_t WEBDAV_BUFF_LEN = 1024 * 256; + static char *webdavBuffer = (char *)heap_caps_malloc(WEBDAV_BUFF_LEN, MALLOC_CAP_SPIRAM); + switch (req->method) + { + case HTTP_GET: + case HTTP_PROPFIND: + { + size_t bytesWritten = webdavFileListingSpiffs(webdavBuffer, WEBDAV_BUFF_LEN, "/dat"); + httpd_resp_send(req, webdavBuffer, bytesWritten); + break; + } + case HTTP_DELETE: + { + httpd_resp_set_hdr(req, "Content-Type", "text/plain"); + + if (portablefs::remove(uriToFileName(uri).c_str())) + httpd_resp_send(req, "Deleted file", -1); + else + httpd_resp_send_404(req); + break; + } + default: + httpd_resp_send_404(req); + } +} \ No newline at end of file diff --git a/firmware/lib/esphttp/WebDAV.h b/firmware/lib/esphttp/WebDAV.h new file mode 100644 index 0000000..192ee47 --- /dev/null +++ b/firmware/lib/esphttp/WebDAV.h @@ -0,0 +1,4 @@ +#include "Dtypes.h" +#include "EspHttp.h" + +void webdavHandler(httpd_req_t *req); \ No newline at end of file diff --git a/firmware/lib/session/MeasurementSession.h b/firmware/lib/session/MeasurementSession.h index a28e38b..08c828b 100644 --- a/firmware/lib/session/MeasurementSession.h +++ b/firmware/lib/session/MeasurementSession.h @@ -39,6 +39,10 @@ public: otherChunk->init(0, 0); } + uint32_t numMeasurements() const { + return currentChunk->getStartIndex() + currentChunk->numMeasurements(); + } + template void serialize(Encoder_T & encoder, uint32_t startIdx) const { diff --git a/firmware/src/ConfigHardware.h b/firmware/src/ConfigHardware.h index c32eda7..389975f 100644 --- a/firmware/src/ConfigHardware.h +++ b/firmware/src/ConfigHardware.h @@ -3,7 +3,7 @@ #include -#define _HW_V_20 +//#define _HW_V_20 // HX711 load cell #ifdef PLATFORM_ESP32 diff --git a/firmware/src/firmware_main.cpp b/firmware/src/firmware_main.cpp index ef25943..4f1136b 100644 --- a/firmware/src/firmware_main.cpp +++ b/firmware/src/firmware_main.cpp @@ -8,7 +8,7 @@ #include #include #endif -#include +//#include #include // for NTP #include // for NTP @@ -23,55 +23,65 @@ #include "ConfigWifi.h" #include "ConfigHardware.h" -#include "AsyncWebDav.h" +#include "EspHttp.h" +#include "WebDAV.h" -AsyncWebServer server(80); WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, "pool.ntp.org"); typedef MeasurementSession Session_T; -template +template class SessionManager { public: SessionManager() : measuring_(false), lastCallTime_(0) - {} - - void begin() { - scale.begin(CONFIG_SCALE_DOUT_PIN, CONFIG_SCALE_SCK_PIN); - scale.tare( CONFIG_TARE_AVG_COUNT ); - session.init( timeClient.getEpochTime() ); + { } - void startMeasurements() { + void begin() + { + Serial.println("Beginning tare"); + scale.begin(CONFIG_SCALE_DOUT_PIN, CONFIG_SCALE_SCK_PIN); + scale.tare(CONFIG_TARE_AVG_COUNT); + Serial.println("Finished tare"); + session.init(timeClient.getEpochTime()); + } + + void startMeasurements() + { measuring_ = true; lastCallTime_ = 0; - session.init( timeClient.getEpochTime() ); + session.init(timeClient.getEpochTime()); } - void stopMeasurements() { + void stopMeasurements() + { measuring_ = false; session.finalize(); } - bool isMeasuring() const { + bool isMeasuring() const + { return measuring_; } - void iteration() { - if( ! measuring_ ) { + void iteration() + { + if (!measuring_) + { //Serial.println("Disabled"); return; } - uint16_t measurement=-1; + uint16_t measurement = -1; scale.measure(measurement); session.addPoint(measurement); Serial.print("Measurement: "); Serial.println(measurement); - if( lastCallTime_ != 0) { + if (lastCallTime_ != 0) + { const long cycleDuration = millis() - lastCallTime_; - if( cycleDuration <= CONFIG_MEASURE_DELAY) + if (cycleDuration <= CONFIG_MEASURE_DELAY) { delay(CONFIG_MEASURE_DELAY - cycleDuration); } @@ -80,8 +90,8 @@ public: const long skipped = (cycleDuration / CONFIG_MEASURE_DELAY); //Serial.printf("Warning: measurements skipped: %d, cycleDuration %d", skipped, cycleDuration); - for(int i=0; i < skipped; ++i) - session.addPoint(measurement); + for (int i = 0; i < skipped; ++i) + session.addPoint(measurement); delay(CONFIG_MEASURE_DELAY * (skipped + 1) - cycleDuration); } @@ -89,7 +99,7 @@ public: lastCallTime_ = millis(); } - Session_T & getSession() { return session; } + Session_T &getSession() { return session; } private: Scale scale; @@ -101,99 +111,105 @@ private: SessionManager sessionManager; -void onNotFound(AsyncWebServerRequest *request) { - request->send(404, "text/plain", "Not found"); -} +EspHttp espHttpServer; -template -void httpSetup(SessionManager * sessionManager) +template +void httpSetup(SessionManager *sessionManager) { - server.on("/api/session/start", HTTP_POST | HTTP_GET, [sessionManager](AsyncWebServerRequest * req) { - AsyncWebServerResponse *response = req->beginResponse(200, "text/plain", F("OK")); - response->addHeader("Access-Control-Allow-Origin", "*"); - req->send(response); - //req->send(200, "text/plain", F("OK")); + auto cbStartSession = [sessionManager](httpd_req_t *req) { + httpd_resp_send(req, "Session started", -1); sessionManager->startMeasurements(); - Serial.println("Started measurements"); - }); - server.on("/api/session/stop", HTTP_POST | HTTP_GET, [sessionManager](AsyncWebServerRequest * req) { - AsyncWebServerResponse *response = req->beginResponse(200, "text/plain", F("OK")); - response->addHeader("Access-Control-Allow-Origin", "*"); - req->send(response); - //req->send(200, "text/plain", F("OK")); + Serial.println("Started session"); + }; + auto cbStopSession = [sessionManager](httpd_req_t *req) { + httpd_resp_send(req, "Session stopped", -1); sessionManager->stopMeasurements(); - Serial.println("Stopped measurements"); - }); - server.on("/api/session/data", HTTP_GET, [sessionManager](AsyncWebServerRequest * req) { - uint32_t startIdx = 0; - if( req->hasParam("startIdx") ) { - startIdx = req->getParam("startIdx")->value().toInt(); - } - Serial.print("Data request, start index: "); - Serial.println(startIdx); + Serial.println("Stopped session"); + }; + auto cbStatus = [sessionManager](httpd_req_t *req) { + String result; + result.reserve(512); + httpd_resp_set_hdr(req, "Content-Type", "application/json"); + // session + { + result += "{ \"session\": "; + if (sessionManager->isMeasuring()) + { + const auto &session = sessionManager->getSession(); + result += "{ \"started\":" + String(session.getStartTime()) + ", "; + result += "\"num_measurements\":" + String(session.numMeasurements()) + "},\n"; + } + else + result += "{},\n"; + } + // flash + { + const String usedBytes(portablefs::usedBytes()); + const String freeBytes(portablefs::totalBytes() - portablefs::usedBytes()); + result += "\"file_system\": { \"used\": " + usedBytes + ", \"free\":" + freeBytes + "},\n"; + } + // RAM + { + const String freeBytes(ESP.getFreeHeap()); + const String usedBytes(ESP.getHeapSize() - ESP.getFreeHeap()); + result += "\"ram\": { \"used\": " + usedBytes + ", \"free\":" + freeBytes + "},\n"; + } + // PSRAM + { + const String freeBytes(ESP.getFreePsram()); + const String usedBytes(ESP.getPsramSize() - ESP.getFreePsram()); + result += "\"psram\": { \"used\": " + usedBytes + ", \"free\":" + freeBytes + "}\n"; + } + result += "}"; + httpd_resp_send(req, result.c_str(), result.length()); + }; + auto cbGetData = [sessionManager](httpd_req_t *req) { + auto sessionId = sessionManager->getSession().getStartTime(); + uint32_t startIdx = getUrlQueryParameter(req, "startIdx", 0); + Serial.printf("Data request, start index: %d", startIdx); + + //headers + httpd_resp_set_hdr(req, "Content-Type", "application/x-msgpack"); + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + httpd_resp_set_hdr(req, "content-disposition", ("attachment; filename=\"" + String(sessionId) + ".st\"").c_str()); + + //data StreamingMsgPackEncoder encoderToDetermineSize(nullptr); encoderToDetermineSize.setSizeCountMode(true); sessionManager->getSession().serialize(encoderToDetermineSize, startIdx); auto totalSize = encoderToDetermineSize.getContentLength(); - Serial.print("Sending started of total size "); - Serial.println(totalSize); - auto callback = [=](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - CopyWriter copyWriter(buffer); - ChunkedStreamingMsgPackEncoder encoder(©Writer, index, index + maxLen); - sessionManager->getSession().serialize(encoder, startIdx); - return encoder.sentBytes() - index; - }; - AsyncWebServerResponse *response = req->beginResponse("application/x-msgpack", totalSize, callback); - response->addHeader("Access-Control-Allow-Origin", "*"); - auto sessionId = sessionManager->getSession().getStartTime(); - response->addHeader("content-disposition", "attachment; filename=\"" + String(sessionId) + ".st\""); - req->send(response); - }); - server.addHandler(new SpiffsWebDavHandler("/webdav", "/dat")); - server.onNotFound(onNotFound); + char *buf = (char *)malloc(totalSize); + CopyWriter copyWriter((uint8_t *)buf); + StreamingMsgPackEncoder encoder(©Writer); + sessionManager->getSession().serialize(encoder, startIdx); + httpd_resp_send(req, buf, totalSize); + free(buf); + }; - server.begin(); + espHttpServer.start(); + espHttpServer.on("/api/session/start", HTTP_GET, cbStartSession); + espHttpServer.on("/api/session/start", HTTP_POST, cbStartSession); + + espHttpServer.on("/api/session/stop", HTTP_GET, cbStopSession); + espHttpServer.on("/api/session/stop", HTTP_POST, cbStopSession); + + espHttpServer.on("/api/session/data", HTTP_GET, cbGetData); + espHttpServer.on("/api/status", HTTP_GET, cbStatus); + + espHttpServer.on("/webdav/*?", HTTP_GET, webdavHandler); + espHttpServer.on("/webdav/*?", HTTP_PROPFIND, webdavHandler); + espHttpServer.on("/webdav/*?", HTTP_DELETE, webdavHandler); } -void listDir(fs::FS &fs, const char * dirname, uint8_t levels){ - Serial.printf("Listing directory: %s\r\n", dirname); - - File root = fs.open(dirname); - if(!root){ - Serial.println("- failed to open directory"); - return; - } - if(!root.isDirectory()){ - Serial.println(" - not a directory"); - return; - } - - File file = root.openNextFile(); - while(file){ - if(file.isDirectory()){ - Serial.print(" DIR : "); - Serial.println(file.name()); - if(levels){ - listDir(fs, file.name(), levels -1); - } - } else { - Serial.print(" FILE: "); - Serial.print(file.name()); - Serial.print("\tSIZE: "); - Serial.println(file.size()); - } - file = root.openNextFile(); - } -} - - void setup() { // Serial Serial.begin(115200); - while(!Serial) {} + while (!Serial) + { + } Serial.println(" "); Serial.println("----- New start -----"); @@ -203,18 +219,27 @@ void setup() printDeviceInfo(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); // WiFi WiFi.mode(WIFI_STA); WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD); - #ifdef PLATFORM_ESP32 +#ifdef PLATFORM_ESP32 WiFi.setHostname(CONFIG_HOSTNAME); - #else +#else WIFI.hostname(CONFIG_HOSTNAME); - #endif +#endif Serial.print(F("\n\n")); Serial.println(F("Waiting for WIFI connection...")); - while (WiFi.status() != WL_CONNECTED) { + int connectCounter = 0; + while (WiFi.status() != WL_CONNECTED) + { delay(1000); + connectCounter += 1; + if (connectCounter > 5) + { + Serial.println("Couldn't obtain WIFI - trying begin() again"); + WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD); + } } Serial.print(F("Connected to WiFi. IP:")); Serial.println(WiFi.localIP()); @@ -229,14 +254,10 @@ void setup() // HTTP & Websocket server httpSetup(&sessionManager); - Serial.println("Spiffs listing:"); - listDir(SPIFFS, "/", 3); - - // - Serial.print("Free Heap"); - Serial.println(ESP.getFreeHeap()); } -void loop() { - sessionManager.iteration(); +void loop() +{ + delay(10000); + //sessionManager.iteration(); }