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.

338 lines
8.4 KiB

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <FS.h>
#include <SPIFFS.h>
#include <ESPAsyncWebServer.h>
#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);
ADC batterySensor(39);
const int8_t speedSensorPin = 13;
const int8_t debugLedPin = 2;
const float wheelDiameterInches = 20;
const int numImpulsesPerTurn = 2;
const float wheelCircumferenceMeters = wheelDiameterInches * 0.0254f * 3.1415f / (float)numImpulsesPerTurn;
uint16_t batteryVoltage = 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;
volatile unsigned long speedSensorRiseTime = 0;
volatile unsigned long speedSensorLastImpulseTime = 0;
volatile unsigned long speedSensorLastImpulseInterval = (unsigned long)-1; // in milliseconds
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);
if(timeSinceLastImpulse < 30)
{
// too little time between impulses, probably some bouncing, ignore it
}
else 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)
{
Serial.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)){
Serial.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)
{
Serial.println("Creating wifi access point...");
WiFi.softAP(wifi_AP_ssid, wifi_AP_password);
Serial.print("Wifi access point created, SSID=");
Serial.print(wifi_AP_ssid);
Serial.print(", IP=");
Serial.println(WiFi.softAPIP());
}
// Also connect as a station (if the configured remote access point is in range)
connectWifi();
OTA.begin();
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){
int v = batteryVoltage;
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,\"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");
server.begin();
Serial.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)
{
Serial.print("Connected to wifi (");
Serial.print(WiFi.SSID().c_str());
Serial.print("), ip=");
Serial.println(WiFi.localIP());
}
else if(newWifiStatus == WL_DISCONNECTED)
{
char codeStr[16];
sprintf(codeStr, "%d", (int)newWifiStatus);
Serial.print("Lost wifi connexion (");
Serial.print(codeStr);
Serial.println(")");
connectWifi();
}
else
{
char codeStr[16];
sprintf(codeStr, "%d", (int)newWifiStatus);
Serial.print("Wifi state: ");
Serial.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 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)
{
if(DataLogger::get().isOpen())
{
Serial.println("Stopping DataLogger");
DataLogger::get().close();
}
}
}
DataLogger::get().log(now, entry);
delay(10);
}