From 01103d0b469367d753c0bb45676ec358559596bc Mon Sep 17 00:00:00 2001 From: Youen Toupin Date: Sun, 27 Mar 2022 23:58:52 +0200 Subject: [PATCH] added data logger --- ESP32/.settings/language.settings.xml | 4 +- ESP32/src/DataLogger.cpp | 84 +++++++++++++++++++ ESP32/src/DataLogger.h | 45 ++++++++++ ESP32/src/utils.cpp | 19 +++++ ESP32/src/utils.h | 4 + ESP32/src/vehicle-monitor.cpp | 115 +++++++++++++++++++++++--- 6 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 ESP32/src/DataLogger.cpp create mode 100644 ESP32/src/DataLogger.h create mode 100644 ESP32/src/utils.cpp create mode 100644 ESP32/src/utils.h diff --git a/ESP32/.settings/language.settings.xml b/ESP32/.settings/language.settings.xml index e473fce..1af7e14 100644 --- a/ESP32/.settings/language.settings.xml +++ b/ESP32/.settings/language.settings.xml @@ -5,7 +5,7 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/ESP32/src/DataLogger.cpp b/ESP32/src/DataLogger.cpp new file mode 100644 index 0000000..a03b632 --- /dev/null +++ b/ESP32/src/DataLogger.cpp @@ -0,0 +1,84 @@ +#include "DataLogger.h" + +#include "utils.h" + +#include + +DataLogger DataLogger::mainLogger; + +DataLogger::DataLogger() +{ +} + +DataLogger::~DataLogger() +{ +} + +void DataLogger::open() +{ + if(isOpen()) return; + + lastLogTime = -std::numeric_limits::max(); + currentTime = 0.0f; + + Preferences preferences; + preferences.begin("vm-log", false); + uint16_t logFileIndex = preferences.getUShort("next", 1); + preferences.putUShort("next", logFileIndex + 1); + preferences.end(); + + // clear existing files until we have enough free space + const size_t requiredSpace = 300000; // in bytes + size_t totalSpace = SPIFFS.totalBytes(); + auto logFolder = SPIFFS.open("/log"); + auto file = logFolder.openNextFile(); + while(file && SPIFFS.usedBytes() + requiredSpace > totalSpace) + { + Serial.print("Deleting old log file: "); + Serial.println(file.name()); + + SPIFFS.remove(file.name()); + file = logFolder.openNextFile(); + } + + char fileName[32]; + sprintf(fileName, "/log/L%05d.csv", logFileIndex); + + file = SPIFFS.open(fileName, FILE_WRITE); + if(!file) Serial.println("DataLogger: failed to open file"); + + if(!file.print("time,speed,battery voltage,battery output current\n")) Serial.println("DataLogger: failed to write to file"); +} + +void DataLogger::close() +{ + if(!isOpen()) return; + file.close(); +} + +void DataLogger::log(unsigned long timeMilliseconds, const Entry& entry) +{ + if(lastLogTime == -std::numeric_limits::max()) lastTimeMilliseconds = timeMilliseconds; + currentTime += (float)utils::elapsed(lastTimeMilliseconds, timeMilliseconds) / 1000.0f; + lastTimeMilliseconds = timeMilliseconds; + + if((lastEntry.isDifferent(entry) || currentTime >= lastLogTime + 20.0f) && currentTime >= lastLogTime + 0.2f) + { + char line[128]; + sprintf(line, "%.3f,%.3f,%.3f,%.3f\n", currentTime, entry.speed, entry.batteryVoltage, entry.batteryOutputCurrent); + file.print(line); + + lastEntry = entry; + lastLogTime = currentTime; + } +} + +bool DataLogger::isOpen() +{ + return file; +} + +const char* DataLogger::currentLogFileName() +{ + return isOpen() ? file.name() : ""; +} diff --git a/ESP32/src/DataLogger.h b/ESP32/src/DataLogger.h new file mode 100644 index 0000000..a0814bb --- /dev/null +++ b/ESP32/src/DataLogger.h @@ -0,0 +1,45 @@ +#include +#include + +class DataLogger +{ +public: + struct Entry + { + float batteryVoltage = 0.0f; // V + float batteryOutputCurrent = 0.0f; // A + float speed = 0.0f; // m/s + + bool isDifferent(const Entry& other) + { + const float scale = speed > 0.0f ? 1.0f : 5.0f; + return + std::abs(batteryVoltage - other.batteryVoltage) > 0.1f * scale + && std::abs(batteryOutputCurrent - other.batteryOutputCurrent) > 0.1f * scale + && std::abs(speed - other.speed) > 0.1f; + } + }; + +public: + DataLogger(); + ~DataLogger(); + + static DataLogger& get() { return mainLogger; } + + void open(); + void close(); + void log(unsigned long timeMilliseconds, const Entry& entry); + + bool isOpen(); + const char* currentLogFileName(); + +private: + static DataLogger mainLogger; + + Entry lastEntry; + unsigned long lastTimeMilliseconds = -1; + float currentTime = 0.0f; + float lastLogTime = 0.0f; + + File file; // @suppress("Abstract class cannot be instantiated") +}; diff --git a/ESP32/src/utils.cpp b/ESP32/src/utils.cpp new file mode 100644 index 0000000..0101de0 --- /dev/null +++ b/ESP32/src/utils.cpp @@ -0,0 +1,19 @@ +#include "utils.h" + +namespace utils +{ + unsigned long elapsed(unsigned long from, unsigned long to) + { + if(to >= from) + { + return to - from; + } + else + { + // if the counter overflowed, this computes the real duration + // of course it won't work if the counter made a "full turn" or more + const unsigned long biggestValue = (unsigned long)-1; + return (biggestValue - from) + to + 1; + } + } +} diff --git a/ESP32/src/utils.h b/ESP32/src/utils.h new file mode 100644 index 0000000..beae2f8 --- /dev/null +++ b/ESP32/src/utils.h @@ -0,0 +1,4 @@ +namespace utils +{ + unsigned long elapsed(unsigned long from, unsigned long to); +} diff --git a/ESP32/src/vehicle-monitor.cpp b/ESP32/src/vehicle-monitor.cpp index b6e1ba0..56a91c5 100644 --- a/ESP32/src/vehicle-monitor.cpp +++ b/ESP32/src/vehicle-monitor.cpp @@ -9,9 +9,13 @@ #include "ADC.h" #include "OTA.h" +#include "DataLogger.h" +#include "utils.h" #include "wifi-credentials.h" +#define DUMMY_DATA 0 + AsyncWebServer server(80); ADC currentSensor(36); @@ -24,13 +28,15 @@ const int numImpulsesPerTurn = 2; const float wheelCircumferenceMeters = wheelDiameterInches * 0.0254f * 3.1415f / (float)numImpulsesPerTurn; uint16_t batteryVoltage = 0; // in mV -uint16_t batteryCurrent = 0; // in mV +uint16_t batteryOutputCurrent = 0; // in mV WiFiMulti wifiMulti; wl_status_t wifi_STA_status = WL_NO_SHIELD; unsigned long wifiConnexionBegin = 0; const unsigned long retryWifiConnexionDelay = 60000; // in milliseconds +unsigned long stoppedSince = -1; + volatile bool debugLedState = true; volatile bool speedSensorState = false; @@ -51,13 +57,13 @@ void IRAM_ATTR onSpeedSensorChange(bool newState) } else { - unsigned long impulseDuration = now > speedSensorRiseTime ? now - speedSensorRiseTime : (4294967295 - speedSensorRiseTime) + now; // if now is lower than speedSensorRiseTime, it means millis() has overflowed (happens every 50 days) + unsigned long impulseDuration = utils::elapsed(speedSensorRiseTime, now); if(impulseDuration > 1000) return; // impulse was too long, ignore it (maybe magnet stopped near the sensor) debugLedState = !debugLedState; digitalWrite(debugLedPin, debugLedState ? HIGH : LOW); - unsigned long timeSinceLastImpulse = now > speedSensorLastImpulseTime ? now - speedSensorLastImpulseTime : (4294967295 - speedSensorLastImpulseTime) + now; + unsigned long timeSinceLastImpulse = utils::elapsed(speedSensorLastImpulseTime, now); speedSensorLastImpulseTime = now; if(timeSinceLastImpulse > 30 && timeSinceLastImpulse < 4000) { @@ -74,11 +80,21 @@ void IRAM_ATTR onSpeedSensorChange() { onSpeedSensorChange(digitalRead(speedSens float getSpeed() { + #if DUMMY_DATA + { + float result = max(0.0f, sinf((float)millis()/30000.0f)) * 7.0f; + return result < 0.25f ? 0.0f : result; + } + #endif + unsigned long now = millis(); + noInterrupts(); unsigned long lastImpulseInterval = speedSensorLastImpulseInterval; unsigned long lastImpulseTime = speedSensorLastImpulseTime; - unsigned long timeSinceLastImpulse = now > lastImpulseTime ? now - lastImpulseTime : (4294967295 - lastImpulseTime) + now; + interrupts(); + + unsigned long timeSinceLastImpulse = utils::elapsed(lastImpulseTime, now); unsigned long interval = timeSinceLastImpulse > lastImpulseInterval * 10 / 9 ? timeSinceLastImpulse : lastImpulseInterval; @@ -144,18 +160,52 @@ void setup() server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){ int v = batteryVoltage; - int c = batteryCurrent; + int c = batteryOutputCurrent; int s = (int)(getSpeed() * 1000.0f + 0.5f); + const char* logFileName = DataLogger::get().currentLogFileName(); + if(String(logFileName).startsWith("/log/")) logFileName += 5; + + int totalSize = (int)(SPIFFS.totalBytes() / 1000); + int usedSize = (int)(SPIFFS.usedBytes() / 1000); + char json[128]; - sprintf(json, "{\"v\":%d,\"c\":%d,\"s\":%d}", v, c, s); + sprintf(json, "{\"v\":%d,\"c\":%d,\"s\":%d,\"log\":\"%s\",\"tot\":%d,\"used\":%d}", v, c, s, logFileName, totalSize, usedSize); request->send(200, "text/json", json); }); + server.on("/api/log/list", HTTP_GET, [](AsyncWebServerRequest *request){ + String json; + + json = "{\"files\":["; + + auto logFolder = SPIFFS.open("/log"); + auto file = logFolder.openNextFile(); + bool first = true; + while(file) + { + if(!first) json += ","; + json += "{\"n\":\"/api"; + json += file.name(); + json += "\",\"s\":"; + json += file.size(); + json += "}"; + + first = false; + file = logFolder.openNextFile(); + } + + json += "]}"; + request->send(200, "text/json", json.c_str()); + }); + // Special case to send index.html without caching server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/www/index.html", "text/html"); }); server.serveStatic("/index.html", SPIFFS, "/www/index.html"); + // Log files (not cached) + server.serveStatic("/api/log", SPIFFS, "/log/"); + // Other static files are cached (index.html knows whether to ignore caching or not for each file) server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=5184000"); @@ -163,10 +213,8 @@ void setup() Serial.println("HTTP server started"); } -void loop() +void handle_wifi_connection() { - OTA.handle(); - wl_status_t newWifiStatus = WiFi.status(); if(newWifiStatus != wifi_STA_status) { @@ -201,11 +249,14 @@ void loop() if(wifi_STA_status != WL_CONNECTED) { unsigned long now = millis(); - unsigned long elapsed = now > wifiConnexionBegin ? now - wifiConnexionBegin : (4294967295 - wifiConnexionBegin) + now; + unsigned long elapsed = utils::elapsed(wifiConnexionBegin, now); if(elapsed > retryWifiConnexionDelay) connectWifi(); } +} +void handle_ADC_measures() +{ const int numSamples = 100; float averageV = 0.0f; @@ -228,9 +279,49 @@ void loop() averageV *= 27.000f; // account for voltage divider to retrieve the input voltage averageC = max(0.0f, averageC - 2.5f) / 0.0238f; // convert voltage to current, according to the sensor linear relation - // TODO: mutex ? batteryVoltage = (uint16_t)(averageV * 1000.0f + 0.5f); - batteryCurrent = (uint16_t)(averageC * 1000.0f + 0.5f); + batteryOutputCurrent = (uint16_t)(averageC * 1000.0f + 0.5f); + + #if DUMMY_DATA + batteryVoltage = (uint16_t)((float)random(4000, 4020) / 100.0f * 1000.0f + 0.5f); + batteryOutputCurrent = (uint16_t)(max(0.0f, sinf((float)millis()/30000.0f)) * 25.0f * 1000.0f + 0.5f); + #endif +} + +void loop() +{ + OTA.handle(); + handle_wifi_connection(); + handle_ADC_measures(); + + unsigned long now = millis(); + static DataLogger::Entry entry; + entry.batteryVoltage = (float)batteryVoltage / 1000.0f; + entry.batteryOutputCurrent = (float)batteryOutputCurrent / 1000.0f; + entry.speed = getSpeed(); + + if(entry.speed > 0.0f) + { + stoppedSince = -1; + if(!DataLogger::get().isOpen()) + { + Serial.println("Starting DataLogger"); + DataLogger::get().open(); + } + } + else + { + if(stoppedSince == -1) + { + stoppedSince = now; + } + else if(utils::elapsed(stoppedSince, now) > 5 * 60 * 1000) + { + Serial.println("Stopping DataLogger"); + DataLogger::get().close(); + } + } + DataLogger::get().log(now, entry); delay(10); }