#include #include "DebugLog.h" #include "ADC.h" #include "OTA.h" #include "DataLogger.h" #include "utils.h" #include #include #include #include #include #include #include "wifi-credentials.h" #include #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::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); }