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.
436 lines
11 KiB
436 lines
11 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; |
|
|
|
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); |
|
}
|
|
|