computing trip statistics on ESP32 (distance, time, elevation, etc.)
This commit is contained in:
parent
98a59085d4
commit
97c2f379b8
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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…
Reference in New Issue
Block a user