From 97c2f379b843fd7b190febbacbf518ba1bf0428a Mon Sep 17 00:00:00 2001 From: Youen Toupin Date: Sun, 17 Apr 2022 12:11:20 +0200 Subject: [PATCH] computing trip statistics on ESP32 (distance, time, elevation, etc.) --- ESP32/src/utils.cpp | 15 ++++ ESP32/src/vehicle-monitor.cpp | 89 ++++++++++++++++--- WebApp/src/monitor-api.ts | 78 +++++++++++++--- WebApp/src/pages/dashboard/dashboard-page.tsx | 52 ++++------- 4 files changed, 175 insertions(+), 59 deletions(-) diff --git a/ESP32/src/utils.cpp b/ESP32/src/utils.cpp index 0332864..73fb803 100644 --- a/ESP32/src/utils.cpp +++ b/ESP32/src/utils.cpp @@ -18,4 +18,19 @@ namespace utils return (biggestValue - from) + to + 1; } } + + uint32_t elapsed(uint32_t from, uint32_t to) + { + if(to >= from) + { + return to - from; + } + else + { + // if the counter overflowed, this computes the real duration + // of course it won't work if the counter made a "full turn" or more + const uint32_t biggestValue = (uint32_t)-1; + return (biggestValue - from) + to + 1; + } + } } diff --git a/ESP32/src/vehicle-monitor.cpp b/ESP32/src/vehicle-monitor.cpp index 465430f..11833bb 100644 --- a/ESP32/src/vehicle-monitor.cpp +++ b/ESP32/src/vehicle-monitor.cpp @@ -33,12 +33,20 @@ 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; @@ -52,6 +60,7 @@ 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; @@ -78,16 +87,21 @@ void IRAM_ATTR onSpeedSensorChange(bool newState) { // 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; + 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; + } } } } @@ -200,17 +214,23 @@ void setup() int v = batteryVoltage; int c = batteryOutputCurrent; int s = (int)(getSpeed() * 1000.0f + 0.5f); - int t = temperature; + 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[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); + 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); }); @@ -399,6 +419,10 @@ void loop() 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; @@ -413,6 +437,11 @@ void loop() { DebugLog.println("Starting DataLogger"); DataLogger::get().open(); + tripDistance = 0; + tripMovingTime = 0; + tripTotalTime = 0; + tripAscendingElevation = 0; + tripMotorEnergy = 0; } } else @@ -432,5 +461,41 @@ void loop() } DataLogger::get().log(now, entry); - delay(DataLogger::get().isOpen() ? 10 : 1000); + 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); } diff --git a/WebApp/src/monitor-api.ts b/WebApp/src/monitor-api.ts index 1db7a6e..3001748 100644 --- a/WebApp/src/monitor-api.ts +++ b/WebApp/src/monitor-api.ts @@ -4,16 +4,26 @@ export interface Status { batteryVoltage: number; // in Volts motorCurrent: number; // in Amperes speed: number; // in meters per second + tripDistance: number; // in meters + tripTotalTime: number; // in seconds + tripMovingTime: number; // in seconds + tripAscendingElevation: number; // in meters + tripMotorEnergy: number; // in Watt-hour temperature: number; // in Celcius degrees altitude: number; // in meters above sea level }; interface ApiStatus { - v: number; - c: number; - s: number; - t: number; - alt: number; + v: number; // voltage (mV) + c: number; // current (mA) + s: number; // speed (mm/s) + td: number; // trip distance (m) + ttt: number; // trip total time (s) + tmt: number; // trip moving time (s) + tae: number; // trip ascending elevation (m) + tme: number; // trip motor energy (Wh) + temp: number; // temperature (tenth of °C) + alt: number; // altitude (mm) } export class MonitorApi { @@ -21,6 +31,15 @@ export class MonitorApi { private lastStatus: Status = null; private static api: MonitorApi = null; + private lastFetchTime = 0; + private lastFetchAltitude = -100000; + + private mockedTripDistance = 0; + private mockedTripTotalTime = 0; + private mockedTripMovingTime = 0; + private mockedTripAscendingElevation = 0; + private mockedTripMotorEnergy = 0; + constructor() { this.mockServer = window.location.protocol == "file:"; } @@ -39,13 +58,43 @@ export class MonitorApi { if(this.mockServer) { await new Promise(resolve => setTimeout(resolve, 200)); - let t = new Date().getTime() / 1000.0; + let t = Date.now() / 1000.0; + if(this.lastFetchTime == 0) this.lastFetchTime = t; + let dt = t - this.lastFetchTime; + this.lastFetchTime = t; + + let speed = (Math.cos(t*0.3)+1.0)*0.5 * 14; // in meters per second + if(speed < 0.25) speed = 0; + + this.mockedTripDistance += speed * dt; + this.mockedTripTotalTime += dt; + if(speed > 0) + this.mockedTripMovingTime += dt; + + let altitude = (Math.cos(t*0.0001)+1.0)*0.5 * 4500 - 200 + (Math.cos(t*0.03))*0.5 * 60; // in meters + if(this.lastFetchAltitude == -100000) this.lastFetchAltitude = altitude; + let altitudeChange = altitude - this.lastFetchAltitude; + this.lastFetchAltitude = altitude; + + if(altitudeChange > 0) + this.mockedTripAscendingElevation += altitudeChange; + + let voltage = (Math.cos(t*0.4)+1.0)*0.5 * 6 + 36; + let current = (Math.cos(t*0.7)+1.0)*0.5 * 30; + let newEnergy = voltage * current * dt / 3600; + this.mockedTripMotorEnergy += newEnergy; + apiStatus = { - v: (Math.cos(t*0.4)+1.0)*0.5 * 6000 + 36000, - c: (Math.cos(t*0.7)+1.0)*0.5 * 30000, - s: (Math.cos(t*0.3)+1.0)*0.5 * 14000, - t: (Math.cos(t*0.2)+1.0)*0.5 * 400 - 100, - alt: (Math.cos(t*0.0001)+1.0)*0.5 * 4500000 - 200000 + (Math.cos(t*0.03))*0.5 * 60000 + v: Math.round(voltage * 1000), + c: Math.round(current * 1000), + s: Math.round(speed * 1000), + td: Math.round(this.mockedTripDistance), + ttt: Math.round(this.mockedTripTotalTime), + tmt: Math.round(this.mockedTripMovingTime), + tae: Math.round(this.mockedTripAscendingElevation*10.0), + tme: Math.round(this.mockedTripMotorEnergy*10.0), + temp: Math.round((Math.cos(t*0.2)+1.0)*0.5 * 400 - 100), + alt: Math.round(altitude * 1000) }; } else { apiStatus = await new Promise((resolve, error) => { @@ -69,7 +118,12 @@ export class MonitorApi { batteryVoltage: apiStatus.v / 1000, motorCurrent: apiStatus.c / 1000, speed: apiStatus.s / 1000, - temperature: apiStatus.t / 10, + tripDistance: apiStatus.td, + tripTotalTime: apiStatus.ttt, + tripMovingTime: apiStatus.tmt, + tripAscendingElevation: apiStatus.tae / 10, + tripMotorEnergy: apiStatus.tme / 10, + temperature: apiStatus.temp / 10, altitude: apiStatus.alt / 1000 }; diff --git a/WebApp/src/pages/dashboard/dashboard-page.tsx b/WebApp/src/pages/dashboard/dashboard-page.tsx index fead0bf..35fc291 100644 --- a/WebApp/src/pages/dashboard/dashboard-page.tsx +++ b/WebApp/src/pages/dashboard/dashboard-page.tsx @@ -19,14 +19,12 @@ export class DashboardPage extends Page { private battery = new Pipe(0.0); private speed = new Pipe(0.0); - private lastRefreshTime = 0.0; - private movementTime = 0.0; - private distance = new Pipe(0.0); - private averageSpeed = new Pipe(0.0); - private energy = new Pipe(0.0); - private averageConsumption = new Pipe(0.0); - private ascendingElevation = new Pipe(0.0); - private lastElevation = -100000; + private tripDistance = new Pipe(0.0); + private tripAverageSpeed = new Pipe(0.0); + private tripEnergy = new Pipe(0.0); + private tripAverageConsumption = new Pipe(0.0); + private tripAscendingElevation = new Pipe(0.0); + private altitude = new Pipe(0.0); private temperature = new Pipe(0.0); @@ -55,30 +53,14 @@ export class DashboardPage extends Page { this.speed.set(this.status.speed * 3.6); // convert m/s to km/h - let now = Date.now() / 1000; - if(this.lastRefreshTime == 0.0) this.lastRefreshTime = now; - let dt = now - this.lastRefreshTime; - this.lastRefreshTime = now; - - if(this.status.speed > 0.0) - this.movementTime += dt; - this.distance.set(this.distance.get() + this.status.speed * dt / 1000); - if(this.movementTime > 0.0) - this.averageSpeed.set(this.distance.get() / (this.movementTime / 3600)); + this.tripDistance.set(this.status.tripDistance/1000); + this.tripAverageSpeed.set(this.status.tripDistance/1000 / (Math.max(1.0, this.status.tripMovingTime)/3600)); + this.tripEnergy.set(this.status.tripMotorEnergy); + this.tripAverageConsumption.set(this.status.tripMotorEnergy / Math.max(0.1, this.status.tripDistance/1000)); + this.tripAscendingElevation.set(this.status.tripAscendingElevation); - if(this.lastElevation == -100000) - this.lastElevation = this.status.altitude; - let elevationChange = this.status.altitude - this.lastElevation; - this.lastElevation = this.status.altitude; - if(elevationChange > 0.0) - this.ascendingElevation.set(this.ascendingElevation.get() + elevationChange); - this.altitude.set(this.status.altitude); - this.temperature.set(this.status.temperature); - - this.energy.set(this.energy.get() + power * dt / 3600); - if(this.distance.get() > 0.1) - this.averageConsumption.set(this.energy.get() / this.distance.get()); + this.altitude.set(this.status.altitude); if(this.autoRefresh) setTimeout(() => { if(this.autoRefresh) this.refresh(); }, 150); @@ -98,15 +80,15 @@ export class DashboardPage extends Page {
- +
- - + +
- - + +