/*
 * 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
}