From 828e7b34054b6df98e83aa1c14697b5737712655 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sun, 21 Jun 2020 16:03:00 +0200 Subject: [PATCH] More cleanup --- firmware/lib/session/SessionManager.h | 118 ++++++++++++++++ .../lib/session/SimpleMeasurementSession.h | 9 +- firmware/src/DeviceInfoLog.h | 75 ---------- firmware/src/firmware_main.cpp | 133 +++--------------- measurement_sizes.ods | Bin 0 -> 10918 bytes 5 files changed, 140 insertions(+), 195 deletions(-) create mode 100644 firmware/lib/session/SessionManager.h delete mode 100644 firmware/src/DeviceInfoLog.h create mode 100644 measurement_sizes.ods diff --git a/firmware/lib/session/SessionManager.h b/firmware/lib/session/SessionManager.h new file mode 100644 index 0000000..8473268 --- /dev/null +++ b/firmware/lib/session/SessionManager.h @@ -0,0 +1,118 @@ + +// NTP headers +#include +#include +#include + +template +class SessionManager +{ +public: + SessionManager(int scaleDoutPin, int scaleSckPin, uint8_t tareAvgCount); + + void begin(); + + void tare(); + void startMeasurements(); + void stopMeasurements(); + bool isMeasuring() const { return measuring_; } + SessionT &session() { return session_; } + + void iteration(); + +private: + WiFiUDP ntpUDP_; + NTPClient timeClient_; + + Scale scale_; + //MockScale scale; + SessionT session_; + bool measuring_; + long lastCallTime_; + + int scaleDoutPin_; + int scaleSckPin_; + uint8_t tareAvgCount_; +}; + +// ------------------------------------------------------------------------------------------------ + +template +SessionManager::SessionManager(int scaleDoutPin, int scaleSckPin, uint8_t tareAvgCount) + : timeClient_(ntpUDP_, "pool.ntp.org"), + measuring_(false), + lastCallTime_(0), + scaleDoutPin_(scaleDoutPin), + scaleSckPin_(scaleSckPin), + tareAvgCount_(tareAvgCount) +{ +} + +template +void SessionManager::tare() +{ + Serial.println("Beginning tare"); + scale_.begin(scaleDoutPin_, scaleSckPin_); + scale_.tare(CONFIG_TARE_AVG_COUNT); + Serial.println("Finished tare"); +} + +template +void SessionManager::begin() +{ + timeClient_.begin(); + timeClient_.update(); + tare(); + session_.init(timeClient_.getEpochTime()); +} + +template +void SessionManager::startMeasurements() +{ + measuring_ = true; + lastCallTime_ = 0; + session_.init(timeClient_.getEpochTime()); +} + +template +void SessionManager::stopMeasurements() +{ + session_.finalize(); + measuring_ = false; +} + +template +void SessionManager::iteration() +{ + if (!measuring_) + return; + + uint16_t measurement = -1; + scale_.measure(measurement); + bool addPointSuccessful = session_.addPoint(measurement); + if (!addPointSuccessful) + { + Serial.println("Maximum time of session reached - stopping"); + stopMeasurements(); + return; + } + if (lastCallTime_ != 0) + { + const long cycleDuration = millis() - lastCallTime_; + if (cycleDuration <= CONFIG_MEASURE_DELAY) + { + delay(CONFIG_MEASURE_DELAY - cycleDuration); + } + else + { + const long skipped = (cycleDuration / CONFIG_MEASURE_DELAY); + Serial.printf("Warning: measurements skipped: %ld, cycleDuration %ld", skipped, cycleDuration); + + for (int i = 0; i < skipped; ++i) + session_.addPoint(measurement); + + delay(CONFIG_MEASURE_DELAY * (skipped + 1) - cycleDuration); + } + } + lastCallTime_ = millis(); +} \ No newline at end of file diff --git a/firmware/lib/session/SimpleMeasurementSession.h b/firmware/lib/session/SimpleMeasurementSession.h index c3fe569..ab61abf 100644 --- a/firmware/lib/session/SimpleMeasurementSession.h +++ b/firmware/lib/session/SimpleMeasurementSession.h @@ -13,6 +13,7 @@ public: : chunk(nullptr), saveInterval_(saveInterval) { } + ~SimpleMeasurementSession() { if (chunk != nullptr) @@ -21,7 +22,7 @@ public: void init(uint32_t epochStartTime) { - if (chunk == nullptr) + if (chunk == nullptr) { // psram allocation doesn't seem to work in constructor chunk = (ChunkT *)heap_caps_malloc(sizeof(ChunkT), MALLOC_CAP_SPIRAM); @@ -35,15 +36,15 @@ public: bool success = chunk->addPoint(measurement); if (success && (chunk->numMeasurements() % saveInterval_) == 0) saveToFileSystem(); - if(!success) + if (!success) Serial.println("Failed to add point"); - //Serial.printf("Add point %d success %d\n", measurement, success); return success; } void finalize() { - saveToFileSystem(); + if (numMeasurements() > 0) + saveToFileSystem(); chunk->init(0, 0); } diff --git a/firmware/src/DeviceInfoLog.h b/firmware/src/DeviceInfoLog.h deleted file mode 100644 index fd93b7c..0000000 --- a/firmware/src/DeviceInfoLog.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifdef USE_ESP32 -#include "SPIFFS.h" -#else -#include -#endif - -inline void printDeviceInfo() -{ - /* - FSInfo fs_info; - SPIFFS.info(fs_info); - - float fileTotalKB = (float)fs_info.totalBytes / 1024.0; - float fileUsedKB = (float)fs_info.usedBytes / 1024.0; - - float flashChipSize = (float)ESP.getFlashChipSize() / 1024.0 / 1024.0; - float realFlashChipSize = (float)ESP.getFlashChipRealSize() / 1024.0 / 1024.0; - float flashFreq = (float)ESP.getFlashChipSpeed() / 1000.0 / 1000.0; - FlashMode_t ideMode = ESP.getFlashChipMode(); - - Serial.printf("\n#####################\n"); - - Serial.printf("__________________________\n\n"); - Serial.println("Firmware: "); - Serial.printf(" Chip Id: %08X\n", ESP.getChipId()); - Serial.print(" Core version: "); - Serial.println(ESP.getCoreVersion()); - Serial.print(" SDK version: "); - Serial.println(ESP.getSdkVersion()); - Serial.print(" Boot version: "); - Serial.println(ESP.getBootVersion()); - Serial.print(" Boot mode: "); - Serial.println(ESP.getBootMode()); - - Serial.printf("__________________________\n\n"); - - Serial.println("Flash chip information: "); - Serial.printf(" Flash chip Id: %08X (for example: Id=001640E0 Manuf=E0, Device=4016 (swap bytes))\n", ESP.getFlashChipId()); - Serial.printf(" Sketch thinks Flash RAM is size: "); - Serial.print(flashChipSize); - Serial.println(" MB"); - Serial.print(" Actual size based on chip Id: "); - Serial.print(realFlashChipSize); - Serial.println(" MB ... given by (2^( Device - 1) / 8 / 1024"); - Serial.print(" Flash frequency: "); - Serial.print(flashFreq); - Serial.println(" MHz"); - Serial.printf(" Flash write mode: %s\n", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT" : ideMode == FM_DIO ? "DIO" : ideMode == FM_DOUT ? "DOUT" : "UNKNOWN")); - - Serial.printf("__________________________\n\n"); - - Serial.println("File system (SPIFFS): "); - Serial.print(" Total KB: "); - Serial.print(fileTotalKB); - Serial.println(" KB"); - Serial.print(" Used KB: "); - Serial.print(fileUsedKB); - Serial.println(" KB"); - Serial.printf(" Block size: %u\n", fs_info.blockSize); - Serial.printf(" Page size: %u\n", fs_info.pageSize); - Serial.printf(" Maximum open files: %u\n", fs_info.maxOpenFiles); - Serial.printf(" Maximum path length: %u\n\n", fs_info.maxPathLength); - - String str = ""; - Dir dir = SPIFFS.openDir("/dat"); - while (dir.next()) - { - str += dir.fileName(); - str += " / "; - str += dir.fileSize(); - str += "\r\n"; - } - Serial.print(str); - */ -} \ No newline at end of file diff --git a/firmware/src/firmware_main.cpp b/firmware/src/firmware_main.cpp index ed59777..2878742 100644 --- a/firmware/src/firmware_main.cpp +++ b/firmware/src/firmware_main.cpp @@ -4,112 +4,24 @@ #include "SwimTrackerConfig.h" #include -#include // for NTP -#include // for NTP // Own libs #include "MockScale.h" #include "Scale.h" #include "MeasurementSession.h" +#include "SessionManager.h" #include "SpiffsStorage.h" -#include "DeviceInfoLog.h" #include "SimpleMeasurementSession.h" #include "EspHttp.h" #include "WebDAV.h" -WiFiUDP ntpUDP; -NTPClient timeClient(ntpUDP, "pool.ntp.org"); - -using Session_T = SimpleMeasurementSession; - -template -class SessionManager -{ -public: - SessionManager() : measuring_(false), lastCallTime_(0) - { - } - - 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()); - Serial.println("Finished session init"); - } - - void startMeasurements() - { - measuring_ = true; - lastCallTime_ = 0; - session.init(timeClient.getEpochTime()); - } - - void stopMeasurements() - { - measuring_ = false; - session.finalize(); - } - - bool isMeasuring() const - { - return measuring_; - } - - void iteration() - { - if (!measuring_) - return; - - uint16_t measurement = -1; - scale.measure(measurement); - bool addPointSuccessful = session.addPoint(measurement); - if(!addPointSuccessful) { - Serial.println("Maximum time of session reached - stopping"); - stopMeasurements(); - return; - } - Serial.print("Measurement: "); - Serial.println(measurement); - if (lastCallTime_ != 0) - { - const long cycleDuration = millis() - lastCallTime_; - if (cycleDuration <= CONFIG_MEASURE_DELAY) - { - delay(CONFIG_MEASURE_DELAY - cycleDuration); - } - else - { - const long skipped = (cycleDuration / CONFIG_MEASURE_DELAY); - Serial.printf("Warning: measurements skipped: %ld, cycleDuration %ld", skipped, cycleDuration); - - for (int i = 0; i < skipped; ++i) - session.addPoint(measurement); - - delay(CONFIG_MEASURE_DELAY * (skipped + 1) - cycleDuration); - } - } - lastCallTime_ = millis(); - } - - Session_T &getSession() { return session; } - -private: - Scale scale; - //MockScale scale; - Session_T session; - bool measuring_; - long lastCallTime_; -}; - -SessionManager sessionManager; +using Session_T = SimpleMeasurementSession; +SessionManager sessionManager(CONFIG_SCALE_DOUT_PIN, CONFIG_SCALE_SCK_PIN, CONFIG_TARE_AVG_COUNT); EspHttp espHttpServer; -template -void httpSetup(SessionManager *sessionManager) +template +void httpSetup(SessionManager *sessionManager) { auto cbStartSession = [sessionManager](httpd_req_t *req) { httpd_resp_send(req, "Session started", -1); @@ -131,7 +43,7 @@ void httpSetup(SessionManager *sessionManager) result += "{ \"session\": "; if (sessionManager->isMeasuring()) { - const auto &session = sessionManager->getSession(); + const auto &session = sessionManager->session(); result += "{ \"started\":" + String(session.getStartTime()) + ", "; result += "\"num_measurements\":" + String(session.numMeasurements()) + "},\n"; } @@ -149,18 +61,18 @@ void httpSetup(SessionManager *sessionManager) 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(); + auto sessionId = sessionManager->session().getStartTime(); uint32_t startIdx = getUrlQueryParameter(req, "startIdx", 0); Serial.printf("Data request, start index: %d", startIdx); @@ -172,13 +84,13 @@ void httpSetup(SessionManager *sessionManager) //data StreamingMsgPackEncoder encoderToDetermineSize(nullptr); encoderToDetermineSize.setSizeCountMode(true); - sessionManager->getSession().serialize(encoderToDetermineSize, startIdx); + sessionManager->session().serialize(encoderToDetermineSize, startIdx); auto totalSize = encoderToDetermineSize.getContentLength(); char *buf = (char *)malloc(totalSize); CopyWriter copyWriter((uint8_t *)buf); StreamingMsgPackEncoder encoder(©Writer); - sessionManager->getSession().serialize(encoder, startIdx); + sessionManager->session().serialize(encoder, startIdx); httpd_resp_send(req, buf, totalSize); free(buf); }; @@ -207,25 +119,19 @@ void setup() while (!Serial) { } - Serial.println(" "); - Serial.println("----- New start -----"); + Serial.println("Starting SwimTracker Firmware"); // File system bool spiffsResult = SPIFFS.begin(true); - Serial.printf("Spiffs begin %d\n", spiffsResult); - - printDeviceInfo(); + if (!spiffsResult) + Serial.println("Failed to mount/format SPIFFS file system"); ESP_ERROR_CHECK(esp_event_loop_create_default()); + // WiFi WiFi.mode(WIFI_STA); WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD); -#ifdef PLATFORM_ESP32 WiFi.setHostname(CONFIG_HOSTNAME); -#else - WIFI.hostname(CONFIG_HOSTNAME); -#endif - Serial.print(F("\n\n")); Serial.println(F("Waiting for WIFI connection...")); int connectCounter = 0; while (WiFi.status() != WL_CONNECTED) @@ -234,23 +140,18 @@ void setup() connectCounter += 1; if (connectCounter > 5) { - Serial.println("Couldn't obtain WIFI - trying begin() again"); + Serial.println("Couldn't obtain WIFI - retrying.."); WiFi.begin(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD); } } - Serial.print(F("Connected to WiFi. IP:")); + Serial.print("Connected to WiFi. IP:"); Serial.println(WiFi.localIP()); - // NTP - timeClient.begin(); - timeClient.update(); - // Session sessionManager.begin(); // HTTP & Websocket server httpSetup(&sessionManager); - } void loop() diff --git a/measurement_sizes.ods b/measurement_sizes.ods new file mode 100644 index 0000000000000000000000000000000000000000..f9945106f59b06a0b8c92139e5feedff0c0e36e3 GIT binary patch literal 10918 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 literal 0 HcmV?d00001