Monitoring system for electric vehicles (log various sensors, such as consumed power, solar production, speed, slope, apparent wind, etc.)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

501 lines
14 KiB

#include <Arduino.h>
#include "DebugLog.h"
#include "ADC.h"
#include "OTA.h"
#include "DataLogger.h"
#include "utils.h"
#include <WiFi.h>
#include <WiFiMulti.h>
#include <FS.h>
#include <SPIFFS.h>
#include <ESPAsyncWebServer.h>
#include <Dps310.h>
#include "wifi-credentials.h"
#include <limits>
#define DUMMY_DATA 0
AsyncWebServer server(80);
ADC currentSensor(36);
ADC batterySensor(39);
Dps310 pressureSensor = Dps310();
const int8_t speedSensorPin = 13;
const int8_t debugLedPin = 2;
const int8_t I2C_SDA = 15;
const int8_t I2C_SCL = 4;
const float wheelDiameterInches = 20;
const int numImpulsesPerTurn = 2;
const float wheelCircumferenceMeters = wheelDiameterInches * 0.0254f * 3.1415f / (float)numImpulsesPerTurn;
const uint32_t wheelCircumferenceMillimeters = (uint32_t)(wheelCircumferenceMeters * 1000.0f + 0.5f);
uint16_t batteryVoltage = 0; // in mV
uint16_t batteryOutputCurrent = 0; // in mV
int16_t temperature = 0; // in tenth of °C
int32_t altitude = 0; // in mm above sea level (can be negative if below sea level, or depending on atmospheric conditions)
// current trip
uint32_t tripDistance = 0; // in meters
uint16_t tripMovingTime = 0; // cumulated seconds, only when moving at non-zero speed
uint16_t tripTotalTime = 0; // total trip time in seconds
uint32_t tripAscendingElevation = 0; // cumulated ascending elevation, in millimeters
uint32_t tripMotorEnergy = 0; // in Joules
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;
volatile unsigned long speedSensorRiseTime = 0;
volatile unsigned long speedSensorLastImpulseTime = 0;
volatile unsigned long speedSensorLastImpulseInterval = (unsigned long)-1; // in milliseconds
volatile uint32_t speedSensorDistance = 0; // Cumulated measured distance, in millimeters. This value will overflow after about 4000km.
void IRAM_ATTR onSpeedSensorChange(bool newState)
{
if(speedSensorState == newState) return;
unsigned long now = millis();
speedSensorState = newState;
bool magnetDetected = !speedSensorState; // the magnet closes the contact which pulls the pin low
if(magnetDetected)
{
speedSensorRiseTime = now;
}
else
{
unsigned long impulseDuration = utils::elapsed(speedSensorRiseTime, now);
if(impulseDuration > 500) return; // impulse was too long, ignore it (maybe magnet stopped near the sensor)
unsigned long timeSinceLastImpulse = utils::elapsed(speedSensorLastImpulseTime, now);
// TODO: find a simple formula that works for any wheel diameter and number of magnets
unsigned long minInterval = speedSensorLastImpulseInterval == (unsigned long)-1 ? 200 : std::min((unsigned long)200, (unsigned long)30 + speedSensorLastImpulseInterval / 2);
if(timeSinceLastImpulse < minInterval)
{
// too little time between impulses, probably some bouncing, ignore it
}
else
{
speedSensorDistance += wheelCircumferenceMillimeters;
if(timeSinceLastImpulse < 4000)
{
speedSensorLastImpulseTime = now;
speedSensorLastImpulseInterval = timeSinceLastImpulse;
}
else
{
// too much time between impulses, can't compute speed from that
speedSensorLastImpulseTime = now;
speedSensorLastImpulseInterval = (unsigned long)-1;
}
}
}
}
void IRAM_ATTR onSpeedSensorChange() { onSpeedSensorChange(digitalRead(speedSensorPin) == HIGH); }
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;
interrupts();
unsigned long timeSinceLastImpulse = utils::elapsed(lastImpulseTime, now);
unsigned long interval = timeSinceLastImpulse > lastImpulseInterval * 10 / 9 ? timeSinceLastImpulse : lastImpulseInterval;
float speed = wheelCircumferenceMeters / (float)interval * 1000.0f; // in meters per second
if(speed < 0.25f)
{
return 0.0f; // if speed is very low (less than 1km/h) it probably means we've stopped
}
return speed;
}
void connectWifi()
{
wifiMulti = WiFiMulti();
const int numSSIDs = sizeof(wifi_STA_credentials)/sizeof(wifi_STA_credentials[0]);
if(numSSIDs > 0)
{
DebugLog.println("Connecting to wifi...");
for(int idx = 0; idx < numSSIDs; ++idx)
{
wifiMulti.addAP(wifi_STA_credentials[idx].SSID, wifi_STA_credentials[idx].password);
}
wifiConnexionBegin = millis();
wifiMulti.run();
}
}
void setup()
{
pinMode(debugLedPin, OUTPUT);
digitalWrite(debugLedPin, HIGH);
pinMode(speedSensorPin, INPUT_PULLUP);
attachInterrupt(speedSensorPin, &onSpeedSensorChange, CHANGE);
Serial.begin(115200);
if(!SPIFFS.begin(false)){
DebugLog.println("SPIFFS Mount Failed");
return;
}
// Set WiFi mode to both AccessPoint and Station
WiFi.mode(WIFI_AP_STA);
// Create the WiFi Access Point
if(wifi_AP_ssid != nullptr)
{
DebugLog.println("Creating wifi access point...");
WiFi.softAP(wifi_AP_ssid, wifi_AP_password);
DebugLog.print("Wifi access point created, SSID=");
DebugLog.print(wifi_AP_ssid);
DebugLog.print(", IP=");
DebugLog.println(WiFi.softAPIP());
}
// Also connect as a station (if the configured remote access point is in range)
connectWifi();
OTA.begin();
Wire.begin(I2C_SDA, I2C_SCL);
pressureSensor.begin(Wire);
server.on("/api/debug/log", HTTP_GET, [](AsyncWebServerRequest *request){
AsyncResponseStream *response = request->beginResponseStream("text/plain");
int logPartIdx = 0;
while(true)
{
const char* text = DebugLog.get(logPartIdx);
if(text[0] == 0) break;
response->print(text);
++logPartIdx;
}
request->send(response);
});
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){
int v = batteryVoltage;
int c = batteryOutputCurrent;
int s = (int)(getSpeed() * 1000.0f + 0.5f);
int temp = temperature;
int alt = altitude;
int td = tripDistance;
int ttt = tripTotalTime;
int tmt = tripMovingTime;
int tae = tripAscendingElevation / 100; // convert mm to dm
int tme = tripMotorEnergy / 360; // convert Joules to dWh (tenth of Wh)
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[256];
sprintf(json, "{\"v\":%d,\"c\":%d,\"s\":%d,\"td\":%d,\"ttt\":%d,\"tmt\":%d,\"tae\":%d,\"tme\":%d,\"temp\":%d,\"alt\":%d,\"log\":\"%s\",\"tot\":%d,\"used\":%d}", v, c, s, td, ttt, tmt, tae, tme, temp, alt, 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");
server.begin();
DebugLog.println("HTTP server started");
digitalWrite(debugLedPin, LOW);
}
void handle_wifi_connection()
{
wl_status_t newWifiStatus = WiFi.status();
if(newWifiStatus != wifi_STA_status)
{
if(newWifiStatus == WL_CONNECTED)
{
DebugLog.print("Connected to wifi (");
DebugLog.print(WiFi.SSID().c_str());
DebugLog.print("), ip=");
DebugLog.println(WiFi.localIP());
}
else if(newWifiStatus == WL_DISCONNECTED)
{
char codeStr[16];
sprintf(codeStr, "%d", (int)newWifiStatus);
DebugLog.print("Lost wifi connexion (");
DebugLog.print(codeStr);
DebugLog.println(")");
connectWifi();
}
else
{
char codeStr[16];
sprintf(codeStr, "%d", (int)newWifiStatus);
DebugLog.print("Wifi state: ");
DebugLog.println(codeStr);
}
wifi_STA_status = newWifiStatus;
}
if(wifi_STA_status != WL_CONNECTED)
{
unsigned long now = millis();
unsigned long elapsed = utils::elapsed(wifiConnexionBegin, now);
if(elapsed > retryWifiConnexionDelay)
connectWifi();
}
}
void handle_ADC_measures()
{
const int numSamples = 100;
float averageV = 0.0f;
float averageC = 0.0f;
for(int sample = 0; sample < numSamples; ++sample)
{
delay(1);
float v = batterySensor.read();
float c = currentSensor.read();
averageV += v;
averageC += c;
}
averageV /= (float)numSamples;
averageC /= (float)numSamples;
if(averageV < 0.2f) averageV = 0.0f;
averageV *= 27.000f; // account for voltage divider to retrieve the input voltage
averageC = std::max(0.0f, averageC - 2.5f) / 0.0238f; // convert voltage to current, according to the sensor linear relation
batteryVoltage = (uint16_t)(averageV * 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 handle_pressure_measure()
{
const uint8_t oversampling = 7;
int16_t ret;
float temp; // in Celcius degrees
ret = pressureSensor.measureTempOnce(temp, oversampling);
if(ret != 0)
{
DebugLog.print("Failed to measure temperature: "); DebugLog.println(ret);
return;
}
temperature = (int16_t)(temp * 10.0f + 0.5f);
float pressure; // in Pa
pressureSensor.measurePressureOnce(pressure, oversampling);
if(ret != 0)
{
DebugLog.print("Failed to measure pressure: "); DebugLog.println(ret);
return;
}
struct PressureToAltitude
{
float pressure; // in Pa
float altitude; // in meters above sea level
};
const PressureToAltitude altitudeTable[] =
{
{ 101325.0f, 0.0f },
{ 100000.0f, 111.0f },
{ 95000.0f, 540.0f },
{ 90000.0f, 989.0f },
{ 85000.0f, 1457.0f },
{ 80000.0f, 1949.0f },
{ 75000.0f, 2466.0f },
{ 70000.0f, 3012.0f },
{ 65000.0f, 3591.0f },
{ 60000.0f, 4206.0f },
{ 55000.0f, 4865.0f },
};
const int8_t altitudeTableNumValues = sizeof(altitudeTable)/sizeof(altitudeTable[0]);
float alt = -std::numeric_limits<float>::max();
for(int8_t i = 0; i < altitudeTableNumValues - 1; ++i)
{
if(i == altitudeTableNumValues - 2 || pressure >= altitudeTable[i+1].pressure)
{
const auto& p = altitudeTable[i];
const auto& n = altitudeTable[i+1];
alt = (pressure - p.pressure) / (n.pressure - p.pressure) * (n.altitude - p.altitude) + p.altitude;
break;
}
}
altitude = (int32_t)(alt * 1000.0f + 0.5f);
/*DebugLog.print("temperature="); DebugLog.print(temp); DebugLog.print("°C");
DebugLog.print(" pressure="); DebugLog.print(pressure); DebugLog.print("Pa");
DebugLog.print(" altitude="); DebugLog.print(altitude); DebugLog.println("mm");*/
}
void loop()
{
OTA.handle();
handle_wifi_connection();
handle_ADC_measures();
handle_pressure_measure(); // also measures temperature
unsigned long now = millis();
static unsigned long lastLoopMillis = now;
unsigned long dt = utils::elapsed(lastLoopMillis, now);
lastLoopMillis = now;
static DataLogger::Entry entry;
entry.batteryVoltage = (float)batteryVoltage / 1000.0f;
entry.batteryOutputCurrent = (float)batteryOutputCurrent / 1000.0f;
entry.speed = getSpeed();
entry.temperature = (float)temperature / 10.0f;
entry.altitude = (float)altitude / 1000.0f;
if(entry.speed > 0.0f)
{
stoppedSince = -1;
if(!DataLogger::get().isOpen())
{
DebugLog.println("Starting DataLogger");
DataLogger::get().open();
tripDistance = 0;
tripMovingTime = 0;
tripTotalTime = 0;
tripAscendingElevation = 0;
tripMotorEnergy = 0;
}
}
else
{
if(stoppedSince == -1)
{
stoppedSince = now;
}
else if(utils::elapsed(stoppedSince, now) > 5 * 60 * 1000)
{
if(DataLogger::get().isOpen())
{
DebugLog.println("Stopping DataLogger");
DataLogger::get().close();
}
}
}
DataLogger::get().log(now, entry);
bool isOnTrip = DataLogger::get().isOpen();
bool isMoving = entry.speed > 0.0f;
static unsigned long cumulatedMillis = 0;
cumulatedMillis += dt;
unsigned long newSeconds = cumulatedMillis / 1000;
cumulatedMillis -= newSeconds * 1000;
uint32_t currentMillimeters = speedSensorDistance;
static uint32_t lastLoopMillimeters = currentMillimeters;
uint32_t newMillimeters = utils::elapsed(lastLoopMillimeters, currentMillimeters);
lastLoopMillimeters = currentMillimeters;
static uint32_t cumulatedMillimeters = 0;
cumulatedMillimeters += newMillimeters;
uint32_t newMeters = cumulatedMillimeters / 1000;
cumulatedMillimeters -= newMeters * 1000;
uint32_t altitudeMillimeters = (uint32_t)(entry.altitude * 1000.0f + 0.5f);
static uint32_t lastLoopAltitude = altitudeMillimeters;
uint32_t altitudeChange = altitudeMillimeters - lastLoopAltitude;
lastLoopAltitude = altitudeMillimeters;
if(isOnTrip)
{
tripTotalTime += newSeconds;
if(isMoving) tripMovingTime += newSeconds;
tripDistance += newMeters;
if(altitudeChange > 0)
tripAscendingElevation += altitudeChange;
uint32_t newEnergy = entry.batteryVoltage * entry.batteryOutputCurrent * ((float)dt / 1000.0f);
tripMotorEnergy += newEnergy;
}
delay(isOnTrip ? 10 : 1000);
}