From 359f3b4cd4c238550522ed8bd6a2a944e52252f5 Mon Sep 17 00:00:00 2001 From: Youen Date: Fri, 24 May 2024 19:31:09 +0200 Subject: [PATCH] Possibility for the client (for example a smartphone) to send current date and time Added possibility to send GPS coordinates as well, but not enabled yet because browsers require an HTTPS connection to enable the geolocation API --- ESP32/src/DataLogger.cpp | 18 ++- ESP32/src/DataLogger.h | 12 +- ESP32/src/vehicle-monitor.cpp | 77 +++++++++++-- WebApp/src/monitor-api.ts | 106 +++++++++++++++++- WebApp/src/pages/dashboard/dashboard-page.tsx | 1 + WebApp/src/pages/raw-data/raw-data-page.tsx | 7 ++ 6 files changed, 207 insertions(+), 14 deletions(-) diff --git a/ESP32/src/DataLogger.cpp b/ESP32/src/DataLogger.cpp index 1d5b068..4f56707 100644 --- a/ESP32/src/DataLogger.cpp +++ b/ESP32/src/DataLogger.cpp @@ -16,7 +16,7 @@ DataLogger::~DataLogger() { } -void DataLogger::open() +void DataLogger::open(const char* currentDateTime) { if(isOpen()) return; @@ -51,7 +51,10 @@ void DataLogger::open() file = SPIFFS.open(fileName, FILE_WRITE); if(!isOpen()) Serial.println("DataLogger: failed to open file"); - if(!file.print("time,speed,battery voltage,battery output current,temperature,altitude\n")) Serial.println("DataLogger: failed to write to file"); + char metadata[64]; + sprintf(metadata, "start time: %s\n", currentDateTime == nullptr || currentDateTime[0] == 0 ? "NA" : currentDateTime); + if(!file.print(metadata)) Serial.println("DataLogger: failed to write to file"); + if(!file.print("time,speed,battery voltage,battery output current,temperature,altitude,latitude,longitude\n")) Serial.println("DataLogger: failed to write to file"); } void DataLogger::close() @@ -69,7 +72,16 @@ void DataLogger::log(unsigned long timeMilliseconds, const Entry& entry) if((lastEntry.isDifferent(entry) || currentTime >= lastLogTime + 20.0f) && currentTime >= lastLogTime + 0.2f) { char line[128]; - sprintf(line, "%.3f,%.3f,%.3f,%.3f,%.1f,%.1f\n", currentTime, entry.speed, entry.batteryVoltage, entry.batteryOutputCurrent, entry.temperature, entry.altitude); + char coords[32]; + if(entry.latitude < -900.0f || !gpsCoordsEnabled) + { + sprintf(coords, "NA,NA"); + } + else + { + sprintf(coords, "%.5f,%.5f", entry.latitude, entry.longitude); + } + sprintf(line, "%.3f,%.3f,%.3f,%.3f,%.1f,%.1f,%s\n", currentTime, entry.speed, entry.batteryVoltage, entry.batteryOutputCurrent, entry.temperature, entry.altitude, coords); file.print(line); if(currentTime >= lastFlushTime + 10.0f) diff --git a/ESP32/src/DataLogger.h b/ESP32/src/DataLogger.h index e015d61..6772771 100644 --- a/ESP32/src/DataLogger.h +++ b/ESP32/src/DataLogger.h @@ -11,6 +11,8 @@ public: float speed = 0.0f; // m/s float temperature = 0.0f; // in °C float altitude = 0.0f; // in m + float latitude = -1000.0f; + float longitude = -1000.0f; bool isDifferent(const Entry& other) { @@ -20,7 +22,9 @@ public: || std::abs(batteryOutputCurrent - other.batteryOutputCurrent) > 0.1f * scale || std::abs(speed - other.speed) > 0.1f || std::abs(temperature - other.temperature) > 0.5f * scale - || std::abs(altitude - other.altitude) > 0.5f * scale; + || std::abs(altitude - other.altitude) > 0.5f * scale + || std::abs(latitude - other.latitude) > 0.0001f + || std::abs(longitude - other.longitude) > 0.0001f; } }; @@ -30,13 +34,15 @@ public: static DataLogger& get() { return mainLogger; } - void open(); + void open(const char* currentDateTime = nullptr); void close(); void log(unsigned long timeMilliseconds, const Entry& entry); bool isOpen(); const char* currentLogFileName(); + void enableGPSCoordinates(bool enable) { gpsCoordsEnabled = enable; } + private: static DataLogger mainLogger; @@ -47,4 +53,6 @@ private: float lastFlushTime = 0.0f; File file; // @suppress("Abstract class cannot be instantiated") + + bool gpsCoordsEnabled = false; }; diff --git a/ESP32/src/vehicle-monitor.cpp b/ESP32/src/vehicle-monitor.cpp index 2a387d4..f59795b 100644 --- a/ESP32/src/vehicle-monitor.cpp +++ b/ESP32/src/vehicle-monitor.cpp @@ -18,7 +18,8 @@ #include -#define DUMMY_DATA 1 +#define DUMMY_DATA 0 +#define ENABLE_GPS_COORDINATES 1 AsyncWebServer server(80); @@ -39,6 +40,10 @@ 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) +float latitude = -1000.0f; // in decimal degrees +float longitude = -1000.0f; // in decimal degrees +float gpsAltitude = -1000.0f; // in meters, above sea level +char realtime[32] = {0}; // UTC date and time, in format YYYY-MM-DDTHH:mm:ss.sssZ // current trip uint32_t tripDistance = 0; // in meters @@ -191,6 +196,10 @@ void setup() OTA.begin(); + #if ENABLE_GPS_COORDINATES + DataLogger::get().enableGPSCoordinates(true); + #endif + Wire.begin(I2C_SDA, I2C_SCL); pressureSensor.begin(Wire); @@ -230,10 +239,54 @@ void setup() 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); + 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,\"lat\":%.5f,\"lng\":%.5f,\"d\":\"%s\"}", v, c, s, td, ttt, tmt, tae, tme, temp, alt, logFileName, totalSize, usedSize, latitude, longitude, realtime); request->send(200, "text/json", json); }); + server.on("/api/info", HTTP_POST, [](AsyncWebServerRequest *request){ + //DebugLog.println("/api/info"); + + /*int params = request->params(); + for(int i=0;igetParam(i); + DebugLog.print(p->name().c_str()); + DebugLog.print("="); + DebugLog.println(p->value().c_str()); + }*/ + + AsyncWebParameter* latitudeParam = request->getParam("lat", true); + AsyncWebParameter* longitudeParam = request->getParam("lng", true); + AsyncWebParameter* altitudeParam = request->getParam("alt", true); + if(latitudeParam != nullptr && longitudeParam != nullptr) + { + char *ending = nullptr; + latitude = strtof(latitudeParam->value().c_str(), &ending); + if (*ending != 0) + latitude = -1000.0f; + longitude = strtof(longitudeParam->value().c_str(), &ending); + if (*ending != 0) + longitude = -1000.0f; + //DebugLog.print("lat="); DebugLog.print(latitude); DebugLog.print(" lng="); DebugLog.println(longitude); + + if(altitudeParam != nullptr) + { + gpsAltitude = strtof(altitudeParam->value().c_str(), &ending); + if (*ending != 0) + gpsAltitude = -1000.0f; + //DebugLog.print("alt="); DebugLog.println(gpsAltitude); + } + } + + AsyncWebParameter* timeParam = request->getParam("time", true); + if(timeParam != nullptr) + { + strcpy(realtime, timeParam->value().c_str()); + //DebugLog.print("time="); DebugLog.println(realtime); + } + + request->send(200); + }); + server.on("/api/log/list", HTTP_GET, [](AsyncWebServerRequest *request){ String json; @@ -435,13 +488,16 @@ void loop() entry.temperature = (float)temperature / 10.0f; entry.altitude = (float)altitude / 1000.0f; + entry.latitude = latitude; + entry.longitude = longitude; + if(entry.speed > 0.0f) { stoppedSince = -1; if(!DataLogger::get().isOpen()) { DebugLog.println("Starting DataLogger"); - DataLogger::get().open(); + DataLogger::get().open(realtime); tripDistance = 0; tripMovingTime = 0; tripTotalTime = 0; @@ -486,8 +542,16 @@ void loop() uint32_t altitudeMillimeters = (uint32_t)(entry.altitude * 1000.0f + 0.5f); static uint32_t lastLoopAltitude = altitudeMillimeters; - uint32_t altitudeChange = altitudeMillimeters - lastLoopAltitude; - lastLoopAltitude = altitudeMillimeters; + uint32_t clampedPositiveAltitudeChange = 0; + if(altitudeMillimeters > lastLoopAltitude + 300) + { + clampedPositiveAltitudeChange = altitudeMillimeters - lastLoopAltitude; + lastLoopAltitude = altitudeMillimeters; + } + else if(lastLoopAltitude > 300 && altitudeMillimeters < lastLoopAltitude - 300) + { + lastLoopAltitude = altitudeMillimeters; + } if(isOnTrip) { @@ -495,8 +559,7 @@ void loop() if(isMoving) tripMovingTime += newSeconds; tripDistance += newMeters; - if(altitudeChange > 0) - tripAscendingElevation += altitudeChange; + tripAscendingElevation += clampedPositiveAltitudeChange; static float remainingEnergy = 0.0f; float newEnergy = entry.batteryVoltage * entry.batteryOutputCurrent * ((float)dt / 1000.0f) + remainingEnergy; diff --git a/WebApp/src/monitor-api.ts b/WebApp/src/monitor-api.ts index 3001748..9ff0631 100644 --- a/WebApp/src/monitor-api.ts +++ b/WebApp/src/monitor-api.ts @@ -10,7 +10,10 @@ export interface Status { tripAscendingElevation: number; // in meters tripMotorEnergy: number; // in Watt-hour temperature: number; // in Celcius degrees + latitude?: number; // latitude (decimal degrees) + longitude?: number; // longitude (decimal degrees) altitude: number; // in meters above sea level + dateTime?: Date; // current date and time (UTC) }; interface ApiStatus { @@ -24,11 +27,29 @@ interface ApiStatus { tme: number; // trip motor energy (Wh) temp: number; // temperature (tenth of °C) alt: number; // altitude (mm) + lat: number; // latitude (in decimal degrees), or -1000 if unknown + lng: number; // longitude (in decimal degrees), or -1000 if unknown + d: string; // current UTC date and time, in format "YYYY-MM-DDTHH:mm:ss.sssZ", or empty string if unknown +} + +export interface Info { + latitude: number; // latitude (decimal degrees) + longitude: number; // longitude (decimal degrees) + altitude: number; // altitude (from sea level, in meters) + time: Date; // UTC date and time +} + +interface ApiInfo { + lat?: number; // latitude (decimal degrees) + lng?: number; // longitude (decimal degrees) + alt?: number; // altitude (from sea level, in meters) + time?: string; // UTC date and time, in format "YYYY-MM-DDTHH:mm:ss.sssZ" } export class MonitorApi { private mockServer: boolean; private lastStatus: Status = null; + private lastKnownPosition: GeolocationPosition = null; private static api: MonitorApi = null; private lastFetchTime = 0; @@ -52,6 +73,81 @@ export class MonitorApi { getStatus() { return this.lastStatus; } + async autoUpdateInfo(): Promise { + if (navigator.geolocation) { + let updatePosition = (position: GeolocationPosition) => { + this.lastKnownPosition = position; + }; + + const options = { + enableHighAccuracy: true, + timeout: 6000, + maximumAge: 3000, + }; + + //navigator.geolocation.getCurrentPosition(updatePosition, () => updatePosition(null), options); + } + + const defaultPosition : GeolocationPosition = { + timestamp: 0, + coords: { + latitude: -1000, + longitude: -1000, + accuracy: -1, + altitude: -1000, + altitudeAccuracy: -1, + heading: null, + speed: null + } + }; + let position = this.lastKnownPosition == null ? defaultPosition : this.lastKnownPosition; + + let info: Info = { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + altitude: position.coords.altitude, + time: new Date() + }; + await this.postInfo(info); + } + + async postInfo(info: Info): Promise { + let apiInfo: ApiInfo = { + lat: info.latitude, + lng: info.longitude, + alt: info.altitude, + time: info.time.toISOString() + }; + + if(this.mockServer) { + await new Promise(resolve => setTimeout(resolve, 200)); + } else { + await new Promise((resolve, error) => { + let request = new XMLHttpRequest(); + request.onreadystatechange = () => { + if(request.readyState == 4) { + if(request.status == 200) { + resolve(); + } + else { + error(); + } + } + }; + + let urlEncodedData = ""; + for(let name in apiInfo) { + if(urlEncodedData != "") urlEncodedData += "&"; + urlEncodedData += encodeURIComponent(name)+'='+encodeURIComponent((apiInfo)[name]); + } + + request.open('POST', '/api/info', true); + request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + request.send(urlEncodedData); + }); + } + } + async fetchStatus(): Promise { let apiStatus: ApiStatus; @@ -94,7 +190,10 @@ export class MonitorApi { 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) + lat: -1000, + lng: -1000, + alt: Math.round(altitude * 1000), + d: '' }; } else { apiStatus = await new Promise((resolve, error) => { @@ -124,7 +223,10 @@ export class MonitorApi { tripAscendingElevation: apiStatus.tae / 10, tripMotorEnergy: apiStatus.tme / 10, temperature: apiStatus.temp / 10, - altitude: apiStatus.alt / 1000 + latitude: apiStatus.lat <= -900 ? null : apiStatus.lat, + longitude: apiStatus.lng <= -900 ? null : apiStatus.lng, + altitude: apiStatus.alt / 1000, + dateTime: apiStatus.d == '' ? null : new Date(apiStatus.d) }; return this.lastStatus; diff --git a/WebApp/src/pages/dashboard/dashboard-page.tsx b/WebApp/src/pages/dashboard/dashboard-page.tsx index 9e087d1..c0541d3 100644 --- a/WebApp/src/pages/dashboard/dashboard-page.tsx +++ b/WebApp/src/pages/dashboard/dashboard-page.tsx @@ -49,6 +49,7 @@ export class DashboardPage extends Page { } async refresh() { + await MonitorApi.get().autoUpdateInfo(); let newStatus = await MonitorApi.get().fetchStatus(); if(this.status == null) m.redraw(); diff --git a/WebApp/src/pages/raw-data/raw-data-page.tsx b/WebApp/src/pages/raw-data/raw-data-page.tsx index 7848f31..7f7522b 100644 --- a/WebApp/src/pages/raw-data/raw-data-page.tsx +++ b/WebApp/src/pages/raw-data/raw-data-page.tsx @@ -18,6 +18,7 @@ export class RawDataPage extends Page { } async refresh() { + await MonitorApi.get().autoUpdateInfo(); this.status = await MonitorApi.get().fetchStatus(); m.redraw(); if(this.autoRefresh) @@ -35,6 +36,12 @@ export class RawDataPage extends Page {

Vitesse : {(this.status.speed * 3.6).toFixed(1)}km/h

Temperature : {this.status.temperature.toFixed(1)}°C

Altitude : {this.status.altitude.toFixed(1)}m

+ {this.status.latitude ? +

Lat {this.status.latitude.toFixed(5)}° Lng {this.status.latitude.toFixed(5)}°

+ : null} + {this.status.dateTime ? +

Time : {this.status.dateTime.toString()}

+ : null} :

Chargement...

; }