|
|
|
#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;
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
// 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 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 t = temperature;
|
|
|
|
int alt = altitude;
|
|
|
|
|
|
|
|
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,\"t\":%d,\"alt\":%d,\"log\":\"%s\",\"tot\":%d,\"used\":%d}", v, c, s, t, 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 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
|
|
delay(DataLogger::get().isOpen() ? 10 : 1000);
|
|
|
|
}
|