#include #include "IDECompat.h" #include #include #include #include #include #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 = 12; 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 > 1000) return; // impulse was too long, ignore it (maybe magnet stopped near the sensor) debugLedState = !debugLedState; digitalWrite(debugLedPin, debugLedState ? HIGH : LOW); unsigned long timeSinceLastImpulse = utils::elapsed(speedSensorLastImpulseTime, now); speedSensorLastImpulseTime = now; if(timeSinceLastImpulse > 30 && timeSinceLastImpulse < 4000) { speedSensorLastImpulseInterval = timeSinceLastImpulse; } else { 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(speedSensorPin, INPUT_PULLUP); attachInterrupt(speedSensorPin, &onSpeedSensorChange, CHANGE); pinMode(debugLedPin, OUTPUT); digitalWrite(debugLedPin, debugLedState ? HIGH : LOW); 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"); } 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 = 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) { Serial.println("Stopping DataLogger"); DataLogger::get().close(); } } DataLogger::get().log(now, entry); delay(10); }