added data logger
This commit is contained in:
parent
22e7612a89
commit
01103d0b46
@ -5,7 +5,7 @@
|
||||
<provider copy-of="extension" id="org.eclipse.cdt.ui.UserLanguageSettingsProvider"/>
|
||||
<provider-reference id="org.eclipse.cdt.core.ReferencedProjectsLanguageSettingsProvider" ref="shared-provider"/>
|
||||
<provider-reference id="org.eclipse.cdt.managedbuilder.core.MBSLanguageSettingsProvider" ref="shared-provider"/>
|
||||
<provider class="org.eclipse.cdt.internal.build.crossgcc.CrossGCCBuiltinSpecsDetector" console="false" env-hash="-387635951242037682" id="org.eclipse.cdt.build.crossgcc.CrossGCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT Cross GCC Built-in Compiler Settings" parameter="${HOME}/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-g++ ${FLAGS} -std=gnu++11 -E -P -v -dD "${INPUTS}"" prefer-non-shared="true">
|
||||
<provider class="org.eclipse.cdt.internal.build.crossgcc.CrossGCCBuiltinSpecsDetector" console="false" env-hash="1001601878207211985" id="org.eclipse.cdt.build.crossgcc.CrossGCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT Cross GCC Built-in Compiler Settings" parameter="${HOME}/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-g++ ${FLAGS} -std=gnu++11 -E -P -v -dD "${INPUTS}"" prefer-non-shared="true">
|
||||
<language-scope id="org.eclipse.cdt.core.gcc"/>
|
||||
<language-scope id="org.eclipse.cdt.core.g++"/>
|
||||
</provider>
|
||||
@ -16,7 +16,7 @@
|
||||
<provider copy-of="extension" id="org.eclipse.cdt.ui.UserLanguageSettingsProvider"/>
|
||||
<provider-reference id="org.eclipse.cdt.core.ReferencedProjectsLanguageSettingsProvider" ref="shared-provider"/>
|
||||
<provider-reference id="org.eclipse.cdt.managedbuilder.core.MBSLanguageSettingsProvider" ref="shared-provider"/>
|
||||
<provider class="org.eclipse.cdt.internal.build.crossgcc.CrossGCCBuiltinSpecsDetector" console="false" env-hash="-387635951242037682" id="org.eclipse.cdt.build.crossgcc.CrossGCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT Cross GCC Built-in Compiler Settings" parameter="${HOME}/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-g++ ${FLAGS} -std=gnu++11 -E -P -v -dD "${INPUTS}"" prefer-non-shared="true">
|
||||
<provider class="org.eclipse.cdt.internal.build.crossgcc.CrossGCCBuiltinSpecsDetector" console="false" env-hash="1001601878207211985" id="org.eclipse.cdt.build.crossgcc.CrossGCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT Cross GCC Built-in Compiler Settings" parameter="${HOME}/.platformio/packages/toolchain-xtensa32/bin/xtensa-esp32-elf-g++ ${FLAGS} -std=gnu++11 -E -P -v -dD "${INPUTS}"" prefer-non-shared="true">
|
||||
<language-scope id="org.eclipse.cdt.core.gcc"/>
|
||||
<language-scope id="org.eclipse.cdt.core.g++"/>
|
||||
</provider>
|
||||
|
84
ESP32/src/DataLogger.cpp
Normal file
84
ESP32/src/DataLogger.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include "DataLogger.h"
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include <Preferences.h>
|
||||
|
||||
DataLogger DataLogger::mainLogger;
|
||||
|
||||
DataLogger::DataLogger()
|
||||
{
|
||||
}
|
||||
|
||||
DataLogger::~DataLogger()
|
||||
{
|
||||
}
|
||||
|
||||
void DataLogger::open()
|
||||
{
|
||||
if(isOpen()) return;
|
||||
|
||||
lastLogTime = -std::numeric_limits<float>::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<float>::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() : "";
|
||||
}
|
45
ESP32/src/DataLogger.h
Normal file
45
ESP32/src/DataLogger.h
Normal file
@ -0,0 +1,45 @@
|
||||
#include <cmath>
|
||||
#include <SPIFFS.h>
|
||||
|
||||
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")
|
||||
};
|
19
ESP32/src/utils.cpp
Normal file
19
ESP32/src/utils.cpp
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
4
ESP32/src/utils.h
Normal file
4
ESP32/src/utils.h
Normal file
@ -0,0 +1,4 @@
|
||||
namespace utils
|
||||
{
|
||||
unsigned long elapsed(unsigned long from, unsigned long to);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user