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);
}