Browse Source

computing trip statistics on ESP32 (distance, time, elevation, etc.)

master
Youen Toupin 2 years ago
parent
commit
97c2f379b8
  1. 15
      ESP32/src/utils.cpp
  2. 89
      ESP32/src/vehicle-monitor.cpp
  3. 78
      WebApp/src/monitor-api.ts
  4. 52
      WebApp/src/pages/dashboard/dashboard-page.tsx

15
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;
}
}
}

89
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);
}

78
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<ApiStatus>((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
};

52
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 {
</div>
<div class="widgets-row">
<NumericValue widgetWidth={0.5} value={this.distance} decimals={1} unit="km" />
<NumericValue widgetWidth={0.5} value={this.tripDistance} decimals={1} unit="km" />
</div>
<div class="widgets-row">
<NumericValue widgetWidth={0.5} value={this.averageSpeed} decimals={1} unit="km/h" />
<NumericValue widgetWidth={0.5} value={this.ascendingElevation} decimals={1} unit="m" />
<NumericValue widgetWidth={0.5} value={this.tripAverageSpeed} decimals={1} unit="km/h" />
<NumericValue widgetWidth={0.5} value={this.tripAscendingElevation} decimals={1} unit="m" />
</div>
<div class="widgets-row">
<NumericValue widgetWidth={0.5} value={this.averageConsumption} decimals={1} unit="Wh/km" />
<NumericValue widgetWidth={0.5} value={this.energy} decimals={1} unit="Wh" />
<NumericValue widgetWidth={0.5} value={this.tripAverageConsumption} decimals={1} unit="Wh/km" />
<NumericValue widgetWidth={0.5} value={this.tripEnergy} decimals={1} unit="Wh" />
</div>
<div class="widgets-row">

Loading…
Cancel
Save