/* * GPS Data Logger for ESP32 * * This program uses an ESP32 microcontroller to gather GPS data and display it on a TFT screen, * as well as serve the data over a web server. The main features include: * * 1. **WiFi Access Point**: The ESP32 creates a WiFi access point that users can connect to in order to view GPS data. * * 2. **GPS Data Retrieval**: The program continuously reads GPS data from a connected GPS module. * - It tracks the current speed and altitude, only displaying the speed if it is 3 MPH or higher. * - If the speed drops below 3 MPH, it displays 0 MPH. * - Altitude changes are only displayed if there is a change of 2 feet or more. * * 3. **Display Information**: The GPS data is displayed on a connected TFT screen, including: * - Current Speed * - Maximum Speed * - Current Altitude (with the 2-foot change rule) * - Maximum Altitude * - Altitude above the starting point * - Average Speed and Total Distance traveled * * 4. **Data Logging**: The program logs maximum speed and altitude data to a file stored in the ESP32's SPIFFS filesystem. * - Daily maximums are saved and can be downloaded as a CSV file from the web server. * * 5. **Web Server**: The program hosts a simple web interface where users can: * - View real-time GPS data. * - Download the logged data as a text file. * * 6. **Real-Time Updates**: The web interface updates every second to display the latest GPS data. * * This code is designed to be used with the Adafruit ST7789 display and the TinyGPS++ library for easy GPS data handling. This code produced by Larren Adams chicodog530@yahoo.com 1-15-2025 */ #include <WiFi.h> #include <WebServer.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7789.h> #include <TinyGPS++.h> #include <HardwareSerial.h> #include <SPIFFS.h> // AP credentials const char* apSSID = "ESP32_GPS_AP"; // Access Point SSID WebServer server(80); // Set server to listen on port 80 // Define the pins for the ST7789 display #define TFT_CS 15 // Chip select pin #define TFT_RST 4 // Reset pin #define TFT_DC 2 // Data/command pin #define TFT_BL 32 // Backlight pin (optional) // Define the pins for the GPS module #define GPS_RX_PIN 16 // GPS RX pin #define GPS_TX_PIN 17 // GPS TX pin TinyGPSPlus gps; HardwareSerial gpsSerial(1); // Use Serial1 for GPS // File to store GPS data const char* gpsDataFile = "/gps_data.txt"; // Max speed and altitude variables float maxSpeed = 0.0; float maxAltitude = 0.0; // Track maximum altitude float startingAltitude = 0.0; // Starting altitude bool startingAltitudeSet = false; // Flag to check if starting altitude is set float totalDistance = 0.0; // Total distance traveled in feet float lastAltitude = 0.0; // Last recorded altitude for distance calculation // Timing variables unsigned long lastDisplayUpdate = 0; const long displayUpdateInterval = 1000; // Update display every 1000 milliseconds (1 second) // Initialize TFT display Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); // Function declarations void fetchGPSData(); void handleRoot(); void handleNotFound(); void handleCurrentData(); void saveDailyMax(); void readDailyMax(); String getCurrentSpeed(); String getCurrentAltitude(); String getAltitudeAboveStart(); void updateDisplay(); void handleDownload(); String getCurrentDate(); float calculateDistance(); String formatDistance(float distanceInFeet); void setup() { Serial.begin(115200); // Set up the open access point WiFi.softAP(apSSID); Serial.println("Access Point started"); Serial.print("IP address: "); Serial.println(WiFi.softAPIP()); // Initialize SPIFFS if (!SPIFFS.begin(true)) { Serial.println("SPIFFS Mount Failed"); return; } // Read previous daily max speed and altitude from file readDailyMax(); // Initialize the TFT display tft.init(135, 240); tft.setRotation(1); tft.fillScreen(ST77XX_BLACK); // Set up the backlight if using pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, HIGH); // Initialize GPS serial gpsSerial.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); // Serve the HTML page server.on("/", handleRoot); server.on("/current_data", handleCurrentData); server.on("/download", handleDownload); server.onNotFound(handleNotFound); server.begin(); Serial.println("Web server started on port 80."); } void loop() { server.handleClient(); fetchGPSData(); if (millis() - lastDisplayUpdate >= displayUpdateInterval) { lastDisplayUpdate = millis(); updateDisplay(); } } void fetchGPSData() { while (gpsSerial.available() > 0) { gps.encode(gpsSerial.read()); // Check if GPS data is valid if (gps.location.isValid()) { if (gps.speed.isValid()) { float currentSpeed = gps.speed.mph(); if (currentSpeed >= 3.0) { if (currentSpeed > maxSpeed) { maxSpeed = currentSpeed; saveDailyMax(); Serial.println("New max speed recorded: " + String(maxSpeed)); } totalDistance += calculateDistance(); // Update total distance } } else { Serial.println("Invalid speed data"); } if (gps.altitude.isValid()) { float currentAltitude = gps.altitude.feet(); if (!startingAltitudeSet) { startingAltitude = currentAltitude; // Set the starting altitude startingAltitudeSet = true; // Mark that the starting altitude is now set Serial.println("Starting altitude set to: " + String(startingAltitude) + " ft"); } // Only update altitude if the change is 2 feet or more if (abs(currentAltitude - lastAltitude) >= 2.0) { lastAltitude = currentAltitude; // Update last altitude if (currentAltitude > maxAltitude) { maxAltitude = currentAltitude; saveDailyMax(); Serial.println("New max altitude recorded: " + String(maxAltitude)); } } } else { Serial.println("Invalid altitude data"); } } else { Serial.println("Invalid location data"); } } } float calculateDistance() { // Calculate distance based on speed and time if (gps.location.isUpdated() && gps.speed.isValid()) { float speedInFeetPerSec = gps.speed.mph() * 5280 / 3600; // Convert speed to feet per second return speedInFeetPerSec * (displayUpdateInterval / 1000.0); // Distance = speed * time } return 0.0; // No distance if speed is invalid } void handleCurrentData() { String data = "{\"currentSpeed\": " + getCurrentSpeed() + ", \"currentAltitude\": " + getCurrentAltitude() + ", \"altitudeAboveStart\": " + getAltitudeAboveStart() + ", \"maxSpeed\": " + String(maxSpeed) + ", \"maxAltitude\": " + String(maxAltitude) + ", \"averageSpeed\": " + String(totalDistance / (millis() / 1000.0)) + // Average speed ", \"totalDistance\": \"" + formatDistance(totalDistance) + "\"}"; // Formatted total distance Serial.println("Sending current data: " + data); // Debug output server.send(200, "application/json", data); } void saveDailyMax() { String dateString = getCurrentDate(); File file = SPIFFS.open(gpsDataFile, FILE_APPEND); if (file) { String data = dateString + ", MAX MPH, " + String(maxSpeed) + ", Max ALT, " + String(maxAltitude) + ", Above Start ALT, " + getAltitudeAboveStart() + ", Total Distance, " + formatDistance(totalDistance) + "\n"; file.print(data); file.close(); Serial.println("Max speed, altitude, and altitude above start saved: " + data); } else { Serial.println("Failed to open file for writing"); } } void readDailyMax() { if (!SPIFFS.exists(gpsDataFile)) { File file = SPIFFS.open(gpsDataFile, FILE_WRITE); if (file) { file.print("Date,MaxSpeed,MaxAltitude,AltitudeAboveStart,TotalDistance\n"); file.close(); Serial.println("Initialized gps_data.txt with header."); } else { Serial.println("Failed to create file for initialization"); } } } void updateDisplay() { if (gps.location.isUpdated()) { tft.fillScreen(ST77XX_BLACK); tft.setTextSize(2); tft.setTextColor(ST77XX_WHITE); tft.setCursor(10, 10); tft.println("Current Speed: " + String((gps.speed.isValid() && gps.speed.mph() >= 3) ? gps.speed.mph() : 0) + " MPH"); tft.println("Max Speed: " + String(maxSpeed) + " MPH"); tft.println("Altitude: " + String(gps.altitude.isValid() ? lastAltitude : 0) + " ft"); tft.println("Max Altitude: " + String(maxAltitude) + " ft"); tft.println("Above Start ALT: " + getAltitudeAboveStart() + " ft"); } } void handleRoot() { String html = "<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>GPS Data</title>"; html += "<style>" "body { font-family: Arial, sans-serif; background-color: #000; color: #fff; text-align: center; }" "h1 { color: #4CAF50; }" "p { font-size: 18px; }" "button { padding: 10px 20px; font-size: 16px; background-color: #4CAF50; color: white; border: none; cursor: pointer; margin-top: 20px; }" "button:hover { background-color: #45a049; }" "#speed { font-size: 48px; color: #4CAF50; animation: pulse 1s infinite; }" "@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }" "</style></head><body>"; html += "<h1>GPS Data</h1>"; html += "<p><strong>Current Speed:</strong> <span id='speed'>0 MPH</span></p>"; html += "<p><strong>Current Altitude:</strong> <span id='currentAltitude'>0 ft</span></p>"; html += "<p><strong>Max Speed:</strong> <span id='maxSpeed'>" + String(maxSpeed) + " MPH</span></p>"; html += "<p><strong>Max Altitude:</strong> <span id='maxAltitude'>" + String(maxAltitude) + " ft</span></p>"; html += "<p><strong>Above Starting Altitude:</strong> <span id='altitudeAboveStart'>0 ft</span></p>"; html += "<p><strong>Average Speed:</strong> <span id='averageSpeed'>0 MPH</span></p>"; html += "<p><strong>Total Distance:</strong> <span id='totalDistance'>0.0 miles</span></p>"; // Add download button html += "<button onclick=\"window.location.href='/download'\">Download Data</button>"; // JavaScript to fetch all data every 1000 ms html += "<script>setInterval(function() { " "fetch('/current_data').then(response => response.json()).then(data => { " "document.getElementById('speed').innerText = data.currentSpeed + ' MPH'; " "document.getElementById('currentAltitude').innerText = data.currentAltitude + ' ft'; " "document.getElementById('maxSpeed').innerText = data.maxSpeed + ' MPH'; " "document.getElementById('maxAltitude').innerText = data.maxAltitude + ' ft'; " "document.getElementById('altitudeAboveStart').innerText = data.altitudeAboveStart + ' ft'; " "document.getElementById('averageSpeed').innerText = (data.totalDistance / (Date.now() / 1000)).toFixed(2) + ' MPH'; " "document.getElementById('totalDistance').innerText = data.totalDistance + ' miles'; " "}); " "}, 1000);</script>"; // Fetch every 1000 ms html += "</body></html>"; server.send(200, "text/html", html); // Send the HTML content } void handleNotFound() { server.send(404, "text/plain", "404: Not Found"); } void handleDownload() { File file = SPIFFS.open(gpsDataFile, FILE_READ); if (file) { server.sendHeader("Content-Disposition", "attachment; filename=gps_data.txt"); server.sendHeader("Content-Type", "text/csv"); server.streamFile(file, "text/csv"); file.close(); } else { server.send(404, "text/plain", "404: File Not Found"); } } String getCurrentSpeed() { return String((gps.speed.isValid() && gps.speed.mph() >= 3) ? gps.speed.mph() : 0); } String getCurrentAltitude() { return gps.altitude.isValid() ? String(lastAltitude) : "Invalid Altitude"; } String getAltitudeAboveStart() { if (gps.altitude.isValid()) { return String(lastAltitude - startingAltitude); } return "Invalid Altitude"; } String getCurrentDate() { if (gps.date.isValid()) { return String(gps.date.month()) + "," + // Month String(gps.date.day()) + "," + // Day String(gps.date.year()); // Year } return "Invalid Date"; } String formatDistance(float distanceInFeet) { float distanceInMiles = distanceInFeet / 5280; // Convert feet to miles return String(distanceInMiles, 1); // Format to one decimal place }