Close
Skip to content

EACP Code v54.3

/****************************************************************************
 * ElectroKev Atomic Clock  
 * Author: Kevin Davy ([email protected])  
 * © 2025 - All Rights Reserved  
 *
 * Written in C++, HTML, Javascript and CSS
 * Hardware: ESP32 Dual Core Microcontroller
 * Crystal: Oven controlled 10MHz Square/Sine Mode
 *
 * Description:  
 * - Pulse-modulated clock synchronized to an external 10 MHz pulse.  
 * - Achieves fine pulse manipulation with accuracy up to 1 microsecond  
 *   per 24 hours for precise timekeeping.  
 * - Segregated NIST time reference for synchronization.  
 * - Manual controls for time and date with pulse injection or removal.  
 * - Rotary encoder used to adjust daily time alteration rate,  
 *   with automatic calculation for microseconds per pulse.  
 * - WiFi connectivity enabled for remote monitoring and control.  
 * - Continuous crystal speed monitoring to debug drift.  
 * - Web interface providing full control over all major clock functions  
 *   via server handlers.  
 * - Advanced fault detection for invalid pulse readings,  
 *   with automatic pulse removal and logging of debug data.  
 * - Regulated and finely tuned power design with individual power supplies  
 *   for stable performance.  
 * - OTA updates for seamless firmware updates over the air.  
 * - Dual-core configuration with timekeeping processes separate from other  
 *   resource-intensive functions for enhanced performance.  
 * - Pulse monitoring inputs configured on high-priority processor interrupts  
 *   for accurate counting.  
 * - Globally accessible NTP time server running on port 123,  
 *   providing highly accurate time synchronization.  
 * - Web based serial console with self monitoring error reporting.  
 *   providing real time debugging and self correction.  
 *
 ***************************************************************************/








/* RUN EVENTS ON CORE 0
   RUN WEBSERVER ON CORE 1
   RUN ESP32 ON CORE 0 
   RUN PULSEPROCESSBUFFER ON CORE 1
   RUN NTP REQUEST SERVER ON 1 */








const float versionNumber = 54.3;




#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.h>
#include <wifiudp.h> 




WiFiUDP ntpUDP; 








/* ──────────────────────────────────────────────────────────────── */
/* 🛰️ NETWORK & NTP CONFIGURATION                                   */
/* ──────────────────────────────────────────────────────────────── */




// Wi-Fi credentials
const char *ssid = SSID;            // Wi-Fi network name
const char *password = PASSWORD;    // Wi-Fi password
char ntpClientIP[16];               // Stores NTP client IP address




// Static IP configuration
IPAddress local_ip(192, 168, 1, 172);  // Local static IP address
IPAddress gateway(192, 168, 1, 1);     // Router gateway IP
IPAddress subnet(255, 255, 255, 0);    // Subnet mask




// Web server setup
AsyncWebServer server(8080);        // Web server on port 8080
Preferences preferences;            // Persistent storage for settings




// NTP Configuration
const char *ntpServer = "time.google.com"; // Default NTP server
//const char *ntpServer = "ntp.my-inbox.co.uk";
const int NTP_PORT = 123;               // NTP protocol port
const long gmtOffset_sec = 1;           // UTC offset (no time zone shift)
const int daylightOffset_sec = 0;       // Daylight saving time offset
long ntpRequests;                        // Counter for NTP requests received
const int NTP_PACKET_SIZE = 48;
byte ntpPacketBuffer[NTP_PACKET_SIZE];




/* ──────────────────────────────────────────────────────────────── */
/* 🏗️ BUILD INFORMATION                                            */
/* ──────────────────────────────────────────────────────────────── */




const char *buildDate = __DATE__;  // Compilation date
const char *buildTime = __TIME__;  // Compilation time




/* ──────────────────────────────────────────────────────────────── */
/* 📌 PIN CONFIGURATION                                           */
/* ──────────────────────────────────────────────────────────────── */




// Rotary Encoder
#define ROTARY_CLK 26   // Clock signal for rotary encoder
#define ROTARY_DT 27    // Data signal for rotary encoder
#define ROTARY_SW 13    // Switch button on rotary encoder




// Time Adjustment Buttons
#define adjustHoursPin 16         // Button to adjust hours
#define adjustMinutesPin 17       // Button to adjust minutes
#define adjustSecondsPin 5        // Button to adjust seconds
#define adjustMicroSecondUp 18    // Button to increase microseconds
#define adjustMicroSecondDown 19  // Button to decrease microseconds




// Other Pins
#define syncNTC 23     // Synchronization input pin
#define pulsePin 32    // Pulse input pin for clock synchronization




#define BUFFER_SIZE 50 // Circular buffer size for pulse tracking




/* ──────────────────────────────────────────────────────────────── */
/* 🌍 GLOBAL OBJECTS                                               */
/* ──────────────────────────────────────────────────────────────── */




UnixTime stamp(0);                // Handles UNIX timestamp calculations
UnixTime nistStamp(0);  //Handles NistTime timestamp calculations
LiquidCrystal_I2C lcd(0x27, 20, 4); // LCD display (I2C, 20x4 characters)




/* ──────────────────────────────────────────────────────────────── */
/* ⏳ CLOCK & TIMEKEEPING VARIABLES                                */
/* ──────────────────────────────────────────────────────────────── */




double microSeconds;                    // Microsecond adjustments
double displayMicroSeconds;              // Displayed microsecond value
double microChange;                      // Microsecond change factor
float rotaryChangeValue = 0.1;           // Step size for rotary encoder adjustments
float timeAdjust;                         // Time correction factor
double pulseDurationBetweenRisingEdges;  // Time between clock pulses
double invalidPulse;                      // Stores invalid pulse duration
double averagePDBRE;                      // Average pulse duration
double totalPDBRE;                        // Sum of pulse durations
double totalPDBREAC = 100000;             // Accumulator for long-term precision
long long espRuntime;                     // ESP32 uptime in seconds
long runtimeSeconds;                      // System runtime in seconds
long runtimeOneMinute;                    // Runtime in minutes
long runtimeHours;                        // Runtime in hours
long runtimeDays;
const int crystalCorrection = 0;          // Correction factor for crystal oscillator
int adjustmentsMade;                      // Total adjustments performed
double xtalFreq;                          // Measured crystal frequency
double totalXtalFreq;                     // Accumulator for frequency calculations
double avgXtalFreq;                       // Average calculated crystal frequency
int lastUpdate;                           // Last update timestamp
String timeNow;                           // Current formatted time as string
unsigned long lastMillis;                 // Last recorded millisecond timestamp
double nistMicros;
double nistAdjuster=0;
double nistCalibrate=0;
double compensateVariable=0;
double oneWayDelay;
double preciseUnix;
unsigned long unixMs;
double totalAdjustedFaultTime;
double adjustedFaultTime;
long nextDatum=60;












/* ──────────────────────────────────────────────────────────────── */
/* ⚠️ FAULT HANDLING VARIABLES                                    */
/* ──────────────────────────────────────────────────────────────── */




int fault;                            // Fault counter
int faultTime;                        // Timestamp of last fault
bool generalFault = true;             // Flag for general faults
String status = "FAULT";              // System status
String faultCode = "Fault: Unexpected Restart"; // Boot Up default message prior to calibration
String flagOtherMessage = " ";        // Stores additional fault messages
double faultyPDBRE;                   // Stores pulse duration of a fault
bool flagInvalidPulse;                 // Flag for invalid pulses
bool flagTimeDrift;                    // Flag for time drift detection
bool flagUnexpectedRestart = true;     // Flag for unexpected ESP32 resets
bool flagNTPReceived;                  // Flag for received NTP requests
bool flagNTPSync;                      // Flag for NTP synchronization success
bool flagVisitor;                      // Flag for visitor activity
bool flagStackSize;                    // Flag for stack size issues
bool flagOther;                        // Generic flag for additional faults
const int MAX_FAULT_LENGTH = 7000;      // Max characters stored in fault log
bool clockLock=false;




/* ──────────────────────────────────────────────────────────────── */
/* 🔄 CIRCULAR BUFFER FOR PULSE TRACKING                          */
/* ──────────────────────────────────────────────────────────────── */




volatile double currentTime;                   // Timestamp of latest pulse
volatile double nextHead;                      // Next head position in buffer
volatile double pulseTimes[BUFFER_SIZE];       // Buffer storing pulse durations
volatile uint32_t bufferHead = 0;              // Head index of circular buffer
volatile uint32_t bufferTail = 0;              // Tail index of circular buffer
double lastRisingEdgeTime;                     // Last recorded rising edge timestamp
int pulseCount;                                // Number of pulses detected
double globalPulseCount;                       // Global pulse count since boot




/* ──────────────────────────────────────────────────────────────── */
/* 📦 GENERAL VARIABLES                                           */
/* ──────────────────────────────────────────────────────────────── */




bool updateDisplayFlag=true;
bool dateSetting = 0;     // Flag for date-setting mode
double nistTime;         // Stores latest NIST time update
double unix;             // Current UNIX timestamp
double differenceTime;    // Difference between NTP and local time
double lastNistUpdate;    // Timestamp of last NIST update
double bootupTime;        // System boot timestamp
int visitorsCount;        // Total visitors to the web interface
int freeMemory = 0;       // Available heap memory
String clientIP;          // Stores latest client IP address
double stackSize;         // Stack size monitoring variable
bool NTPON=true;
String mode;          // Stores latest client IP address








// --- Interrupt Service Routine (ISR) ---
IRAM_ATTR void pulseISR() {
 currentTime=esp_timer_get_time();
  nextHead = (bufferHead + 1) % BUFFER_SIZE;
  if (nextHead != bufferTail) {  // Check if the buffer is not full
    pulseTimes[bufferHead] = currentTime;
    bufferHead = nextHead;
  }  
}








// --- Setup Function ---
void setup() {




    mode="Testing";




    Serial.begin(115200);




    // --- WiFi and OTA Setup ---
    setupWifi();  // Starts WiFi and web server and NTP server (runs on Core 0)




    Serial.println("Starting OTA...");
    ArduinoOTA.begin();
    Serial.println("OTA Updates Ready");
    ArduinoOTA.setPassword(nullptr);  // Disable password




    // --- Rotary Encoder Pins Setup ---
    pinMode(ROTARY_CLK, INPUT);
    pinMode(ROTARY_DT, INPUT);
    pinMode(ROTARY_SW, INPUT_PULLUP);




    // --- Pulse Pin Setup ---
    pinMode(pulsePin, INPUT);
    attachInterrupt(digitalPinToInterrupt(pulsePin), pulseISR, FALLING);




    // --- LCD Initialization ---
    lcd.init();
    lcd.begin(20, 4);
    lcd.backlight();




    // --- Button Pins Setup ---
    pinMode(adjustHoursPin, INPUT_PULLUP);
    pinMode(adjustMinutesPin, INPUT_PULLUP);
    pinMode(adjustSecondsPin, INPUT_PULLUP);
    pinMode(adjustMicroSecondDown, INPUT_PULLUP);
    pinMode(adjustMicroSecondUp, INPUT_PULLUP);
    pinMode(syncNTC, INPUT_PULLUP);




    // --- Load Stored Data ---
    loadFromEEPROM();




    // --- Task: Process NTP Requests --- //start this back up
    xTaskCreatePinnedToCore(
        taskProcessNTPRequests,  // Task function
        "ProcessNTPRequests",    // Task name
        8096,                   // Stack size
        NULL,                    // Task parameters
        4,                       // Priority (higher than pulse processing)
        NULL,                    // Task handle
        1                        // Core 
    );




        // --- Task: Main Buffer Processor
    xTaskCreatePinnedToCore(
        taskProcessPulseBuffer,  // Task function
        "ProcessPulses",         // Task name
        4096,                   // Stack size
        NULL,                    // Task parameters
        4,                       // Priority (higher than pulse processing)
        NULL,                    // Task handle
        1                        // Core 
    );




    xTaskCreatePinnedToCore(
         ntpRestartTask,   // Task function
         "NTP_Restart",    // Task name
         4048,             // Stack size
         NULL,             // Task parameters
         1,                // Priority
         NULL,             // Task handle
         1                 // Core 
);




    xTaskCreatePinnedToCore(
         taskUpdateDisplay,   // Task function
         "DisplayUpdate",    // Task name
         8048,             // Stack size
         NULL,             // Task parameters
         1,                // Priority
         NULL,             // Task handle
         1                 // Core 
);








    xTaskCreatePinnedToCore(
        taskOneHourSelfCheck,  // Task function
        "SelfCheck",    // Task name
        4096,                   // Stack size
        NULL,                    // Task parameters
        1,                       // Priority 
        NULL,                    // Task handle
        1                        // Core 
    );








    // --- Startup Confirmation ---
    Serial.println("Running");
    flagUnexpectedRestart = true;
    faultHandler();




        // --- Sync Time from NTP ---
    syncWithNTP();
   // stamp.getDateTime(unix);
}




// --- Main Loop ---
void loop() {
  handleButtons();        // Check and process button inputs
  handleRotaryEncoder();  // Handle rotary encoder inputs
  adjustmentRoutine();    // Adjust time if microseconds overflow
  ArduinoOTA.handle();
  vTaskDelay(100);
}




void taskProcessPulseBuffer(void *pvParameters) { 
  while (true) {
    processPulseBuffer();
    vTaskDelay(pdMS_TO_TICKS(100));  // limit function execution frequency
  }
}




void taskUpdateDisplay(void *pvParameters) { 
  // Run indefinitely with a 0.1-second delay
  while (true) {
    if (updateDisplayFlag){
    updateDisplay();  // Call the function to update the internal screen.
    updateDisplayFlag=false;
    }
    vTaskDelay(pdMS_TO_TICKS(20));  // limit function execution frequency
  }
}




void taskOneHourSelfCheck(void *pvParameters) { 
  while (true) {
  vTaskDelay(pdMS_TO_TICKS(3600000)); 
            saveToEEPROM();
            String allOK="| Automatic Self Check: "+status+ " | Drift: "+differenceTime;
            totalPDBREAC = 0;
            totalPDBRE = 0;
            runtimeHours++;
            runtimeDays=runtimeHours/24;
            averagePDBRE = 0;
            totalPDBREAC=100000;
            avgXtalFreq = 0;
            totalXtalFreq = 0;
            globalPulseCount = 0;
            runtimeSeconds = 0;
            customMessage(allOK);
  }
}








void taskProcessNTPRequests(void *pvParameters) {
  while (true) {
    processNTPRequests();  // Call the function to process NTP requests
    //checkStack();
    vTaskDelay(pdMS_TO_TICKS(50));  // limit function execution frequency
  }
}




void ntpRestartTask(void *pvParameters) {
    while (true) {
        vTaskDelay(1800000 / portTICK_PERIOD_MS);  // Wait for 1 hour
        ntpUDP.stop();  // Stop current UDP server
        server.end();
        vTaskDelay(100 / portTICK_PERIOD_MS);  // Short delay
        server.begin();
        ntpUDP.begin(NTP_PORT);  // Restart NTP server
        customMessage("NTP Check: OK");
    }
}








void faultCheck(){
  generalFault = flagUnexpectedRestart || flagTimeDrift; // Set generalFault
  status = generalFault ? "FAULT" : "OK   ";  // Update status
}








void faultHandler() {




        // If faultCode exceeds max length, trim the oldest part
    if (faultCode.length() > MAX_FAULT_LENGTH) {
        faultCode = faultCode.substring(0, MAX_FAULT_LENGTH);
    }




    // Handle unexpected restart
    if (flagUnexpectedRestart) {
        esp_reset_reason_t resetReason = esp_reset_reason();
        String resetReasonText;




        switch (resetReason) {
            case ESP_RST_POWERON: resetReasonText = "POWERON"; break;
            case ESP_RST_BROWNOUT: resetReasonText = "BROWNOUT"; break;
            case ESP_RST_SW: resetReasonText = "SW"; break;
            case ESP_RST_EXT: resetReasonText = "EXT"; break;
            case ESP_RST_PANIC: resetReasonText = "MEMPANIC"; break;
            case ESP_RST_WDT: resetReasonText = "WATCHDOG"; break;
            default: resetReasonText = "UNKNOWN"; break;
        }




        faultCode = timeNow + "   Fault: Unexpected Restart ESP_RST_" + resetReasonText + 
                    " " + String(espRuntime) + " Sec\n" + faultCode;
        generalFault = true;
        flagUnexpectedRestart = false;
    }




    // Handle specific faults
    if (flagTimeDrift) {
        faultCode = timeNow + "   Fault: Time Drift -> " + String(differenceTime) + "\n" + faultCode;
        generalFault = true;
        flagTimeDrift = false;
    }
    if (flagInvalidPulse) {
        faultCode = timeNow + "   Warning: Invalid Pulse Detected (" + String(invalidPulse) + ")\n" + faultCode;
        flagInvalidPulse = false;
    }




    // Log events (without triggering generalFault)
    if (flagNTPReceived) {
        faultCode = timeNow + "   NTP Request Received from [IP: " + ntpClientIP + "]\n" + faultCode;
        ntpRequests++;
        flagNTPReceived = false;
    }
    if (flagNTPSync) {
        faultCode = timeNow + "   NTP Successfully Synced\n" + faultCode;
        flagNTPSync = false;
    }
    if (flagVisitor) {
        faultCode = timeNow + "   Web User Logged In [IP:" + clientIP + "]\n" + faultCode;
        flagVisitor = false;
    }
    if (flagStackSize) {
        faultCode = timeNow + "   WARNING: StackSize Remaining: " + String(stackSize) + " bytes\n" + faultCode;
        flagStackSize = false;
    }
    if (flagOther) {
        faultCode = timeNow + "   Message: " + flagOtherMessage + "\n" + faultCode;
        flagOtherMessage.clear();
        flagOther = false;
    }








    // Save fault status and update system status
    status = generalFault ? "FAULT" : "OK   ";
    saveToEEPROM();
}








void customMessage(String cm){
  flagOther=true;
  flagOtherMessage=cm;
  faultHandler();
}








void processPulseBuffer() {




if (!clockLock){




    while (bufferTail != bufferHead) {
        // Disable interrupts temporarily to process the pulse
        noInterrupts();
        double pulseTime = pulseTimes[bufferTail];
        bufferTail = (bufferTail + 1) % BUFFER_SIZE;
        pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime) + crystalCorrection;
        lastRisingEdgeTime = pulseTime;
        interrupts();  // Re-enable interrupts




        // Initialize variables at startup
        if (runtimeSeconds == 0) {
            globalPulseCount = 9;
            totalXtalFreq = 90;
            //fault = 0;
        }




        // Validate the pulse duration
        if (pulseDurationBetweenRisingEdges >= 60000 && pulseDurationBetweenRisingEdges <= 140000) {
            pulseCount++;
            unix=unix+0.100;
            nistTime=nistTime+0.100;
            // Calculate crystal frequency 
            xtalFreq = 1000000 / pulseDurationBetweenRisingEdges;
        } else {
            invalidPulse = pulseDurationBetweenRisingEdges;
            adjustedFaultTime = invalidPulse/1000000.0;
            totalAdjustedFaultTime += adjustedFaultTime;
            unix += adjustedFaultTime;
            fault++;
            flagInvalidPulse = true;
            faultHandler();
        }




        // Update crystal frequency tracking
        if (runtimeSeconds > 0) {
            totalXtalFreq += xtalFreq;
            avgXtalFreq = totalXtalFreq / globalPulseCount;
        }




        globalPulseCount++;




        // Perform specific high frequency tasks
        if (runtimeOneMinute >= 60) {
           //checkDifference();
           nistUpdate(); 
           lastNistUpdate = nistTime;
           runtimeOneMinute = 0;
           
        }




        // Reset after 1 hour
        if (runtimeSeconds >= 3600) {
        }




        // Process accumulated pulses (every 10 pulses)
        if (pulseCount >= 10) {
           // Update time and display once per successful 10-pulse cycle
            updateTime();   
            nextDatum-= 1;
            checkDifference();
            reportVariables();
            if (runtimeSeconds > 0) {
                totalPDBREAC += pulseDurationBetweenRisingEdges;  
                totalPDBRE = ((runtimeSeconds + 1) * 100000) - totalPDBREAC;
                averagePDBRE = totalPDBREAC / (runtimeSeconds + 1);
            }
            // Track system runtime
            runtimeSeconds++;
            pulseCount = 0;
            espRuntime++;
            runtimeOneMinute++;
            microSeconds += (microChange * 10);
            freeMemory = ESP.getFreeHeap();
            checkStack();
            timeAdjustCorrections();   
            
        }   
    }
} // closes clockLock
} 




void timeAdjustCorrections(){
   // Adjust display for remaining microseconds per hour
            if (timeAdjust != 0) {
                if (microSeconds < 0) {
                    displayMicroSeconds = round((100000 + microSeconds) / (10 * abs(microChange) * 3600) * 1000) / 1000;
                } else if (microSeconds > 0) {
                    displayMicroSeconds = round((100000 - microSeconds) / (10 * microChange * 3600) * 1000) / 1000;
                }




                // Update microseconds adjustment
                microChange = ((timeAdjust * 1000000) / 864000);




                // Correct very small timeAdjust values close to 0
                if (abs(timeAdjust) < 0.1) {
                    timeAdjust = 0.0;
                    microSeconds = 0;
                    microChange = 0;
                    displayMicroSeconds = 0;
                }
            } 
}








void checkDifference() {
        
        differenceTime = (unix - nistTime);  








 if (abs(differenceTime) >= 5) {
  if (!flagTimeDrift) { //ensure error only runs once per major fault
            flagTimeDrift = true;
            faultHandler();
            customMessage("Automatic Repair Disabled - Fault Logged");
            //nistUpdate();
            return;
        }
       
    }
}
    




void reportVariables(){
          Serial.print(" Difference Time ");
        Serial.print(differenceTime,6);
        Serial.print(" Runtime Seconds ");
        Serial.print(runtimeSeconds);
        Serial.print(" UNIX Time ");
        Serial.print(unix,6);
        Serial.print(" NIST Time ");
        Serial.print(nistTime,6);
        Serial.print(" Compensation ");
        Serial.println(compensateVariable);
}








void processNTPRequests() {
if (!NTPON){  
    int packetSize = ntpUDP.parsePacket();




    if (packetSize == 0) return; // No new packet




    if (packetSize < 48) {
        customMessage("NTP request rejected. Packet too small: Flushed");
        ntpUDP.flush();
        return; // Ignore invalid packets
    }




    if (packetSize > 48) {
        customMessage("NTP request rejected. Packet too large: Flushed");
        ntpUDP.flush();
        return; // Ignore invalid packets
    }




    Serial.print("NTP request received: ");




    byte packetBuffer[48]; // Create response buffer
    memset(packetBuffer, 0, 48);




    ntpUDP.read(packetBuffer, packetSize); // Read the request




// Ensure valid NTP request (mode 3 - client mode)
byte mode = packetBuffer[0] & 0b00000011; // Extract the last 3 bits (mode)
if (mode != 3) {  // Check if it's a client request (mode 3)
    customMessage("NTP request rejected. Packet Mode Invalid: Flushed");
    ntpUDP.flush();
    return;
}




    // Copy client's transmit timestamp (bytes 40-47) into origin timestamp (bytes 24-31)
    memcpy(&packetBuffer[24], &packetBuffer[40], 8);




    // Set response header: LI = 0 (No Warning), Version = 4, Mode = 4 (Server)
    packetBuffer[0] = 0b00100100;




    // Stratum 2 (Secondary time source)
    packetBuffer[1] = 2;




    // Polling interval 
    packetBuffer[2] = 6;




    // Precision declaration
    packetBuffer[3] = -20;




    // Root Delay (Fixed-point format, 1ms)
    packetBuffer[4] = 0x00;
    packetBuffer[5] = 0x01;
    packetBuffer[6] = 0x00;
    packetBuffer[7] = 0x00;




    // Root Dispersion (Fixed-point, 1)
    packetBuffer[8] = 0x00;
    packetBuffer[9] = 0x01;
    packetBuffer[10] = 0x00;
    packetBuffer[11] = 0x00;




    // Reference Identifier
    packetBuffer[12] = 'G';
    packetBuffer[13] = 'P';
    packetBuffer[14] = 'S';
    packetBuffer[15] = 0;




// Get the current epoch time
unsigned long currentEpoch = unix;  
unsigned long ntpSeconds = currentEpoch + 2208988800UL;




// Convert pulseCount (1/10s increments) into NTP fractional format
unsigned long ntpFractionSend = (pulseCount * 429496729) % 4294967296UL;




// Reference Timestamp (last time this clock was updated)
packetBuffer[16] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[17] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[18] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[19] = ntpSeconds & 0xFF;
packetBuffer[20] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[21] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[22] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[23] = ntpFractionSend & 0xFF;




// Receive Timestamp (time request was received)
packetBuffer[32] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[33] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[34] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[35] = ntpSeconds & 0xFF;
packetBuffer[36] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[37] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[38] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[39] = ntpFractionSend & 0xFF;




// Transmit Timestamp (time response is sent)
packetBuffer[40] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[41] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[42] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[43] = ntpSeconds & 0xFF;
packetBuffer[44] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[45] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[46] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[47] = ntpFractionSend & 0xFF;




    // Debug Output
    Serial.print("NTP Response Sent: ");
    Serial.print(ntpSeconds);
    Serial.print(".");
    Serial.print(ntpFractionSend);
    Serial.print(" IP:");




    // Send the response
    ntpUDP.beginPacket(ntpUDP.remoteIP(), ntpUDP.remotePort());
    ntpUDP.write(packetBuffer, 48);
    ntpUDP.endPacket();




    // Retrieve and print the IP address of the requesting client
    Serial.println(ntpUDP.remoteIP());




    // get client IP for NTP
    ntpUDP.remoteIP().toString().toCharArray(ntpClientIP, sizeof(ntpClientIP));
    flagNTPReceived = true;
    faultHandler();
}
}




void checkStack(){
  // Check stack size
        stackSize = uxTaskGetStackHighWaterMark(NULL);
        if (stackSize < 1400) {
            flagStackSize = true;
            faultHandler();
        }
}








/// --- Handle Rotary Encoder Inputs ---
void handleRotaryEncoder() {
  static int lastEncoderState = HIGH;
  int currentEncoderState = digitalRead(ROTARY_CLK);
  static unsigned long lastEncoderChangeTime = 0;
  const unsigned long debounceDelay = 80;  // Increased debounce delay




  // Check for debounce timing
  if ((millis() - lastEncoderChangeTime) > debounceDelay) {
    if (currentEncoderState != lastEncoderState) {
      // If the direction is different, increment or decrement the value by the desired rotaryChangeValue
      if (digitalRead(ROTARY_DT) != currentEncoderState) {
        timeAdjust += 0.1;  // Adjust by 0.1
      } else {
        timeAdjust -= 0.1;  // Adjust by -0.1
      }




      // Update the last change time
      lastEncoderChangeTime = millis();
    }
  }
  // Update the last encoder state
  lastEncoderState = currentEncoderState;
}




// --- Handle Button Inputs ---
void handleButtons() {
  static unsigned long lastButtonPressTime = 0;
  const unsigned long buttonDebounceInterval = 150;




  // Check for debounce interval
  if (millis() - lastButtonPressTime > buttonDebounceInterval) {




    // Toggle dateSetting if rotary switch is pressed
    if (digitalRead(ROTARY_SW) == LOW) {
      saveToEEPROM();
      dateSetting = !dateSetting;  // Toggle the value of dateSetting
    }




    // Adjust time or date based on dateSetting state
    if (!dateSetting) {
      // Time adjustment mode
      if (digitalRead(adjustHoursPin) == LOW) {
        unix += 3600;  // Add one hour
      } else if (digitalRead(adjustMinutesPin) == LOW) {
        unix += 60;  // Add one minute
      } else if (digitalRead(adjustSecondsPin) == LOW) {
        unix -= stamp.second;  // Reset seconds
        pulseCount = 0;
      } else if (digitalRead(adjustMicroSecondDown) == LOW) {
        pulseCount--;  // Decrease pulse count
      } else if (digitalRead(adjustMicroSecondUp) == LOW) {
        pulseCount++;  // Increase pulse count
      } else if (digitalRead(syncNTC) == LOW) {
        lcd.clear();
        lcd.print("WiFi Toggle Button Pressed");




        if (WiFi.status() == WL_CONNECTED) {
          lcd.print("Turning WiFi OFF...");
          WiFi.disconnect(true);
          WiFi.mode(WIFI_OFF);
        } else {
          lcd.print("Turning WiFi ON...");
          WiFi.mode(WIFI_STA);
          WiFi.begin(ssid, password);
        }




        vTaskDelay(pdMS_TO_TICKS(500));
        lcd.clear();
      }
    } else if (dateSetting) {
      // Date adjustment mode
      if (digitalRead(adjustHoursPin) == LOW) {
        unix += 86400;  // Add one day
      } else if (digitalRead(adjustMinutesPin) == LOW) {
        unix += 2592000;  // Add one month (needs refinement for proper month handling)
      } else if (digitalRead(adjustSecondsPin) == LOW) {
        unix += 31622400;  // Add one year
      } else if (digitalRead(adjustMicroSecondDown) == LOW) {
        unix = 1736521548;
      }
    }




    // Update date and time
    stamp.getDateTime(unix);




    // Update debounce timer
    lastButtonPressTime = millis();
  }
}




// --- Update Time ---
void updateTime() {
  //unix++;
  //nistTime++;
  stamp.getDateTime(unix);
  // Extract the fractional part for milliseconds
  unixMs = (unix - floor(unix)) * 1000;
  // Format time string including milliseconds
  timeNow = "[" + String(formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second) + "." + String(unixMs)) + "]";
  updateDisplayFlag=true;
}




// --- Update Display ---
void updateDisplay() {




  if (dateSetting) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Date Setting Mode");
    lcd.setCursor(0, 1);
    lcd.print("Adj:");
    lcd.print(timeAdjust, 1);
    lcd.print(" ");
    lcd.print(displayMicroSeconds);  // Display positive microseconds
    return;
  }




  // Display full time and date
  lcd.setCursor(0, 0);
  displayFullTimeDate();




  // Display UTC time
  lcd.setCursor(0, 1);
  lcd.print("UTC: ");
  lcd.print(unix,3);  // Display Unix timestamp
  lcd.setCursor(0, 2);
  lcd.print("NTP: ");
  lcd.print(nistTime,3);




  // Display calculated crystal frequency
  lcd.setCursor(0, 3);
  lcd.print(avgXtalFreq, 6);
  lcd.print(" MHz ");




  // Display pulse adjustments
  lcd.setCursor(14, 3);
  lcd.print(status);
}




// --- Adjustment Routine ---
void adjustmentRoutine() {
  if (microSeconds >= 100000) {
    microSeconds = 0;
    //pulseCount++;
    unix=unix+0.1;
    adjustmentsMade++;
  } else if (microSeconds <= -100000) {
    microSeconds = 0;
    //pulseCount--;
    unix=unix-0.1;
    adjustmentsMade++;
  }
}








void loadFromEEPROM() {
  preferences.begin("clock", true);  // Open in read-only mode
  microChange = preferences.getInt("microChange", 0);  // Default value 0
  timeAdjust = preferences.getFloat("timeAdjust", 0);
  faultCode = preferences.getString("faultCode", faultCode);
  runtimeHours = preferences.getDouble("runtimeHours", 0);
  ntpRequests = preferences.getLong("ntpRequests");
  preferences.end();
  
}




void saveToEEPROM() {
  preferences.begin("clock", false);  // Open a namespace (false = read/write mode)
  preferences.putInt("microChange", microChange);
  preferences.putString("faultCode", faultCode);
  preferences.putFloat("timeAdjust", timeAdjust);
  preferences.putDouble("runtimeHours", runtimeHours);
  preferences.putLong("ntpRequests",ntpRequests);
  preferences.end();  // Close the Preferences storage
  
}




// --- Format Time ---
String formatTime(int timeValue) {
  return (timeValue < 10) ? "0" + String(timeValue) : String(timeValue);
}




void displayFullTimeDate() {
  // Get the current time and date from the Unix timestamp
  int currentYear = (stamp.year);      // Current year
  int currentMonth = (stamp.month);    // Current month (1-12)
  int currentDay = (stamp.day);        // Current day
  int currentHour = (stamp.hour);      // Current hour
  int currentMinute = (stamp.minute);  // Current minute
  int currentSecond = (stamp.second);  // Current second




  // Display time and date on the LCD
  lcd.print(currentDay);
  lcd.print("/");
  lcd.print(currentMonth);
  lcd.print("/");
  lcd.print(currentYear);




  // lcd.setCursor(0, 1);
  lcd.print(" ");
  lcd.print(formatTime(currentHour));
  lcd.print(":");
  lcd.print(formatTime(currentMinute));
  lcd.print(":");
  lcd.print(formatTime(currentSecond));
}




// --- Display Variables in Serial Monitor  ---
void displayVariables() {
  Serial.print(" RT: ");
  Serial.print(runtimeSeconds);
  Serial.print(" PC: ");
  Serial.print(pulseCount);
  Serial.print(" GPC: ");
  Serial.print(globalPulseCount);
  Serial.print(" PDBRE: ");
  Serial.print(pulseDurationBetweenRisingEdges);
  Serial.print(" unix: ");
  Serial.print(unix);
  Serial.print(" nistTime: ");
  Serial.print(nistTime);
  Serial.print(" dT: ");
  Serial.print(differenceTime);
  Serial.print(" xTalFreq: ");
  Serial.print(xtalFreq, 6);
  Serial.print(" totalXF: ");
  Serial.print(totalXtalFreq, 6);
  Serial.print(" avgXF: ");
  Serial.println(avgXtalFreq, 6);
}




void setupWifi() {
  // Begin WiFi connection
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  // Display WiFi connection details
  Serial.println("Connected to WiFi");
  Serial.println(WiFi.localIP());
  WiFi.setSleep(false);




  // Set up route handlers
  routeHandlers();




  // Start the server
  server.begin();
  Serial.println("Server started");
  
  ntpUDP.begin(NTP_PORT); 
  Serial.println("NTP Server started on port 123");
}












void handleVariables(AsyncWebServerRequest *request) {
  // Create a JSON document (fixed size)
  StaticJsonDocument<1024> doc;




  // Populate the json string
  doc["unixTime"] = String(unix,3);
  doc["bootupTime"] = bootupTime;
  doc["nistTime"] = String(nistTime,3);
  doc["date"] = String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year);
  doc["time"] = formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second);
  doc["displayMicroSeconds"] = displayMicroSeconds;
  doc["microChange"] = microChange;
  doc["pulseDurationBetweenRisingEdges"] = pulseDurationBetweenRisingEdges;
  doc["timeAdjust"] = timeAdjust;
  doc["differenceTime"] = String(differenceTime,6);
  doc["freeMemory"] = freeMemory;
  doc["averagePDBRE"] = String(averagePDBRE, 2);
  doc["runtimeSeconds"] = runtimeSeconds;
  doc["adjustmentsMade"] = adjustmentsMade;
  doc["xtalFreq"] = String(xtalFreq, 6);
  doc["avgXtalFreq"] = String(avgXtalFreq, 6);
  doc["fault"] = fault;
  doc["globalPulseCount"] = globalPulseCount;
  doc["totalPDBRE"] = totalPDBRE;
  doc["espRuntime"] = espRuntime;
  doc["lastNistUpdate"] =formatTime(nistStamp.hour) + ":" + formatTime(nistStamp.minute) + ":" + formatTime(nistStamp.second);
  doc["runtimeHours"] = runtimeHours;
  doc["runtimeDays"] = runtimeDays;
  doc["faultCode"] = String(faultCode);
  doc["status"] = String(status);
  doc["stackSize"] = String(stackSize);
  doc["ntpRequests"] = String(ntpRequests);
  doc["nistAdjuster"] = String(nistAdjuster);
  doc["compensateVariable"] = String(compensateVariable);
  doc["oneWayDelay"] = String(oneWayDelay);
  doc["mode"] = String(mode);
  doc["totalAdjustedFaultTime"] = String(totalAdjustedFaultTime);
  doc["nextDatum"] = String(nextDatum);
  




  // Serialize the JSON document to a string
  String json;
  serializeJson(doc, json);




  // Send the JSON response
  request->send(200, "application/json", json);
}




void nistUpdate() {
    NTPON = true;
    nextDatum=60;
    Serial.print("NIST Update: ");




    memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE); // Clear buffer




    unsigned long sendTime = millis(); // Record send time
    sendNTPpacket(ntpServer);




    unsigned long startWait = millis();
    bool received = false;
    unsigned long recvTime = 0;




    while (millis() - startWait < 800) {
        if (ntpUDP.parsePacket()) {
            recvTime = millis(); // Record receive time
            received = true;
            break;
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // Small delay to avoid CPU overload
    }




    if (received) {
        ntpUDP.read(ntpPacketBuffer, NTP_PACKET_SIZE);




        uint32_t ntpSeconds = (ntpPacketBuffer[40] << 24) | (ntpPacketBuffer[41] << 16) | 
                              (ntpPacketBuffer[42] << 8) | ntpPacketBuffer[43];




        uint32_t ntpFraction = (ntpPacketBuffer[44] << 24) | (ntpPacketBuffer[45] << 16) | 
                               (ntpPacketBuffer[46] << 8) | ntpPacketBuffer[47];




        // Convert NTP time (from 1900 epoch) to Unix time (from 1970)
        uint32_t unixSeconds = ntpSeconds - 2208988800UL;
        double microseconds = ((double)ntpFraction * 1000000.0) / 4294967296.0;




        // Calculate RTT and one-way delay estimate
        double rtt_ms = recvTime - sendTime;  // Total round-trip time (milliseconds)
       // oneWayDelay = rtt_ms / 2000.0; // Convert to seconds (half of RTT)
        oneWayDelay = rtt_ms / 1000.0; // Convert to seconds (full RTT)




        // Apply compensation
        nistTime = (unixSeconds + (microseconds / 1000000.0)) - oneWayDelay + compensateVariable;
        nistStamp.getDateTime(nistTime);




        // Print delay information
        Serial.printf("Round Trip Time: %.2f ms : ", rtt_ms);
        Serial.printf("Estimated One-Way Delay: %.6f s : ", oneWayDelay);
        Serial.println("NIST Update Successful");
    } else {
        Serial.println("Failed to NIST UPDATE.");
    }




    NTPON = false;
}




void syncWithNTP() {
    clockLock = true; // lock the clock so it doesn't run whilst being synced for the first time
    NTPON = true; // lock off NTP requests whilst UDP is being used to recieve time
    visitorsCount = 0; // Reset visitor count
    compensateVariable = 0; // Reset tare variable for compensating following calibration
    nistAdjuster = 0; // Reset fine-tuned micro up/down 
    Serial.println("Connecting to NTP server...");




    // Clear the buffer before sending the request
    memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE);




    // Send an NTP request
    sendNTPpacket(ntpServer);




    unsigned long startWait = millis();
    bool received = false;




    // Wait for up to 800ms, but exit as soon as a response is received
    while (millis() - startWait < 500) {
        if (ntpUDP.parsePacket()) {
            received = true;
            break;
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // Small delay to avoid CPU overload
    }




    if (received) {
        ntpUDP.read(ntpPacketBuffer, NTP_PACKET_SIZE);




        // Extract the 64-bit NTP timestamp (Seconds + Fraction)
        uint32_t ntpSeconds = (ntpPacketBuffer[40] << 24) | (ntpPacketBuffer[41] << 16) | 
                              (ntpPacketBuffer[42] << 8) | ntpPacketBuffer[43];




        uint32_t ntpFraction = (ntpPacketBuffer[44] << 24) | (ntpPacketBuffer[45] << 16) | 
                               (ntpPacketBuffer[46] << 8) | ntpPacketBuffer[47];




        // Convert NTP time (from 1900 epoch) to Unix time (from 1970)
        uint32_t unixSeconds = ntpSeconds - 2208988800UL;




        // Convert fraction to microseconds
        double microseconds = (ntpFraction * 1000000.0) / 4294967296.0; 




        // Store Unix time with microseconds
        nistTime = unixSeconds + (microseconds / 1000000.0);
        
        // Disable interrupts briefly for updating unix
        portDISABLE_INTERRUPTS();
        unix = nistTime;
        portENABLE_INTERRUPTS();




        checkDifference();  // Call the function to compare time against reference
        
        lastNistUpdate = nistTime;
        bootupTime = unix;




        Serial.print("NTP Sync Successful: ");
        Serial.print(microseconds);
        Serial.print(" Microseconds ");
        Serial.println(nistTime, 6);  // Print with 6 decimal places
        pulseCount=floor(microseconds/100000); // calibrate synced fractional seconds to pulseCount so unix starts from exact point mid second
        flagNTPSync = true;
        faultHandler();




        // Update LCD
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("NTP Sync OK!");
    } else {
        Serial.println("Failed to get time from NTP server.");
        lcd.clear();
        lcd.print("NTP Sync Failed!");
    }




    NTPON = false;    // allow NTP requests to recommence
    clockLock = false; //restart the clock
}








void sendNTPpacket(const char* address) {
    memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE);  // Clear packet before filling it
    ntpPacketBuffer[0] = 0b11100011;  //  set to mode 3 for transmission




    // Send the NTP packet
    IPAddress ntpIP;
    WiFi.hostByName(address, ntpIP);  
    ntpUDP.begin(123);  
    ntpUDP.beginPacket(ntpIP, 123);
    ntpUDP.write(ntpPacketBuffer, NTP_PACKET_SIZE);
    ntpUDP.endPacket();
}




void fullReset(){
    Wire.end();
    vTaskDelay(100);
    Wire.begin();
    lcd.begin(20,4);
    generalFault=false;
    fault = 0;
    flagInvalidPulse=false;
    flagUnexpectedRestart=false;
    flagTimeDrift=false;
    flagNTPReceived=false;
    flagNTPSync=false;
    flagVisitor=false;
    flagStackSize=false;
    flagOther=false;
    faultCode="Status: OK : StackSize = "+String(stackSize)+"\n"+"NTP Server Started(Port: [123]): OK"+"\n"+"Connection Started(clock.electrokev.com): OK"+"\n"+"Server Started(Port: [80]): OK"+"\n";
    totalPDBRE = 0;
    totalPDBREAC=100000;
    runtimeSeconds = 0;
    runtimeOneMinute=0;
    runtimeHours=0;
    runtimeDays=0;
    espRuntime =0;
    averagePDBRE = 0;
    xtalFreq = 0;
    avgXtalFreq = 0;
    totalXtalFreq = 0;
    globalPulseCount = 0;
    totalAdjustedFaultTime=0;
    nextDatum=60;
    //compensateVariable=0;
    //nistAdjuster=0;
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Full Reset");
    saveToEEPROM();
}




void routeHandlers() {




  // Define route handlers
  server.on("/", HTTP_GET, handleRoot);
  server.on("/variables", HTTP_GET, handleVariables);




  // Synchronize time with NTP
  server.on("/syncNTP", HTTP_GET, [](AsyncWebServerRequest *request) {
    syncWithNTP();
    request->send(200, "text/plain", "NTP Sync Completed");
  });




  // Increase pulse count
  server.on("/microUP", HTTP_GET, [](AsyncWebServerRequest *request) {
    //pulseCount++;
    unix=unix+0.1;
    nistAdjuster+=0.1;
   //unix+=0.1;
    /*lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("MicroUp");*/
    request->send(200, "text/plain", "Pulse Count Increased");
  });




  // Decrease pulse count
  server.on("/microDOWN", HTTP_GET, [](AsyncWebServerRequest *request) {
    unix=unix-0.1;
    //pulseCount--;
    nistAdjuster-=0.1;
    //unix-=0.1;
    /*lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("MicroDown");*/
    request->send(200, "text/plain", "Pulse Count Decreased");
  });




  // Increase time adjust
  server.on("/timeAdjustINC", HTTP_GET, [](AsyncWebServerRequest *request) {
    timeAdjust += 0.1;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("timeAdjust INC");
    request->send(200, "text/plain", "Time Adjust Increased");
  });




  // Decrease time adjust
  server.on("/timeAdjustDEC", HTTP_GET, [](AsyncWebServerRequest *request) {
    timeAdjust -= 0.1;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("timeAdjust DEC");
    request->send(200, "text/plain", "Time Adjust Decreased");
  });




  // Get version number
  server.on("/getVersion", HTTP_GET, [](AsyncWebServerRequest *request) {
    String version = String(versionNumber, 1);
    request->send(200, "application/json", "{\"version\":\"" + version + "\"}");
  });




  // Lock data to EEPROM and update NIST
  server.on("/lockEEPROM", HTTP_GET, [](AsyncWebServerRequest *request) {
    saveToEEPROM();
    nistUpdate();
    request->send(200, "text/plain", "Saved to EEPROM");
  });




  // Reset buffer
  server.on("/bufferReset", HTTP_GET, [](AsyncWebServerRequest *request) {
 fullReset();
    request->send(200, "text/plain", "Buffer Reset");
  });




  // Restart the ESP32
  server.on("/neverPress", HTTP_GET, [](AsyncWebServerRequest *request) {
    ESP.restart();
    request->send(200, "text/plain", "Never Press");
  });








  // compensate Up
  server.on("/compensateUp", HTTP_GET, [](AsyncWebServerRequest *request) {
    compensateVariable+=0.1;
    request->send(200, "text/plain", "compensateUp");
  });




  // compensate down
  server.on("/compensateDown", HTTP_GET, [](AsyncWebServerRequest *request) {
    compensateVariable-=0.1;
    request->send(200, "text/plain", "compensateDown");
  });




    // auto tare 
  server.on("/autoTare", HTTP_GET, [](AsyncWebServerRequest *request) {
    compensateVariable=differenceTime;
    customMessage("Auto Tare Activated: " + String(differenceTime));
    request->send(200, "text/plain", "autoTare");
  });




    // mode select 
  server.on("/modeSelect", HTTP_GET, [](AsyncWebServerRequest *request) {
    if (mode=="Testing"){
        mode="Normal";
    }
    else {
      mode="Testing";
    }
    customMessage("Mode Change: " + String(mode));
    request->send(200, "text/plain", "modeSelect");
});




}




void handleRoot(AsyncWebServerRequest *request) {
  clientIP=request->client()->remoteIP().toString();
  visitorsCount++;
  flagVisitor=true;
  faultHandler();
 
  String index_html = R"rawliteral(
   


    <meta charset="UTF-8">
    <title>ElectroKev Atomic Clock EACP</title>
    <style>
body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    margin: 0;
    background: linear-gradient(135deg, #1f1f1f, #333);
    color: #e0e0e0;
    font-family: 'Arial', sans-serif;
}




.time {
    font-size: 4rem;
    font-weight: bold;
    text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
    margin-bottom: 20px;
    color: #00ff00;
}




.container {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: #222;
    color: #fff;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
    max-width: 800px;
    margin: 20px auto;
    text-align: center;
    width: 100%;
}




.variables {
    display: grid;
    grid-template-columns: repeat(3, 2fr);
    gap: 10px;
    width: 100%;
    max-width: 800px;
    text-align: left;
    padding: 5px;
}




.variable {
    font-size: 1rem;
    padding: 12px 20px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.1);
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}




.version-container {
    margin-top: 20px;
    display: flex;
    justify-content: center;
    gap: 10px;
    font-size: 1.1rem;
}




.version {
    color: #fff;
    font-weight: bold;
}




.last-updated {
    color: #00ff00;
    font-weight: normal;
}




.ntp {
    color: orange;
    font-weight: normal;
}




.footer {
    margin-top: 30px;
    font-size: 1.2rem;
    text-align: center;
    color: #ccc;
}




.footer a,
.mainlinks a {
    color: #00ff00;
    text-decoration: none;
    font-weight: bold;
}




.footer a:hover {
    text-decoration: underline;
}




.description {
    font-size: 1.2rem;
    text-align: left;
    color: #e0e0e0;
    padding: 20px;
    background: rgba(0, 0, 0, 0.7);
    border-radius: 15px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
    margin-top: 30px;
    width: 100%;
    max-width: 800px;
}




.description h3 {
    font-size: 1.5rem;
    font-weight: bold;
    color: #ffdd00;
}




.admin-controls {
    text-align: center;
    margin: 20px;
}




.serial-container {
    margin-top: 30px;
    width: 100%;
    max-width: 800px;
    height: 300px;
    background-color: #1e1e1e;
    color: #00ff00;
    border-radius: 10px;
    padding: 10px;
    overflow-y: scroll;
    box-shadow: 0px 0px 15px rgba(0, 255, 0, 0.1);
    font-size: 22px;
    white-space: pre-wrap;
}




.serial-output {
    font-family: 'Courier New', monospace;
    font-size: 10px;
    margin: 0;
    padding: 0;
    line-height: 1.5;
}




.serial-output div {
    padding: 2px;
}




.timestamps {
    color: #ffdd00;
    font-weight: bold;
}




.header {
    font-size: 28px;
    font-weight: bold;
    margin-bottom: 5px;
}




.header2 {
    font-size: 16px;
    font-weight: normal;
    color: #ffa500;
}




.subheader {
    font-size: 16px;
    color: #ccc;
    margin-bottom: 15px;
}




.version-container,
.ntp-container,
.bootup-container {
    margin-top: 10px;
    font-size: 16px;
}




.status-label {
    font-weight: bold;
    margin-left: 10px; /*this one for main serial output */
    color: #ffffff;
}




.status {
    font-weight: bold;
}




.status[data-status="OK"] {
    color: #00ff00;
}




.status[data-status="FAULT"] {
    color: #ff0000;
}




/* Responsive Adjustments */
@media (max-width: 600px) {
    .time { font-size: 6rem; }
    .variable { font-size: 14px; }
    .header, .header2 { font-size: 1rem; }
    .footer { font-size: 1.5rem; }
    .description { font-size: 1rem; }
}




@media (max-width: 1024px) {
    .time { font-size: 6rem; }
    .variable { font-size: 24px; }
    .header, .header2 { font-size: 3rem; }
    .footer { font-size: 1.5rem; }
    .description { font-size: 1rem; }
}








button {
    font-family: 'Arial', sans-serif;
    font-size: 0.8rem;
    padding: 12px 20px;
    border: none;
    border-radius: 8px;
    background: rgba(0, 255, 0, 0.2); /* Transparent green effect */
    color: #00ff00;
    cursor: pointer;
    transition: all 0.3s ease-in-out;
    backdrop-filter: blur(8px); /* Glass effect */
    border: 1px solid rgba(0, 255, 0, 0.4);
    box-shadow: 0 4px 10px rgba(0, 255, 0, 0.2);
    text-transform: uppercase;
    font-weight: bold;
    letter-spacing: 1px;
    width: 100%; /* Ensures uniform width */
    max-width: 200px; /* Prevents stretching */
}




/* Hover Effect */
button:hover {
    background: rgba(0, 255, 0, 0.3);
    box-shadow: 0 6px 15px rgba(0, 255, 0, 0.4);
    transform: translateY(-2px);
}




/* Active Button (When Clicked) */
button:active {
    background: rgba(0, 255, 0, 0.4);
    box-shadow: 0 2px 5px rgba(0, 255, 0, 0.5);
    transform: scale(0.98);
}




/* Disabled Button */
button:disabled {
    background: rgba(255, 255, 255, 0.2);
    color: rgba(255, 255, 255, 0.5);
    border: 1px solid rgba(255, 255, 255, 0.3);
    cursor: not-allowed;
    box-shadow: none;
}




/* Focus Effect */
button:focus {
    outline: none;
    box-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
}




/* Button Container & Grid */
.button-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    margin-top: 20px;
}




/* Fixed Grid for Buttons */
.button-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); /* Ensures proper wrapping */
    gap: 10px;
    justify-items: center;
    width: 100%; /* Makes sure the grid expands correctly */
    max-width: 600px; /* Prevents excessive stretching */
    margin: 0 auto; /* Centers the grid */
}




/* Mobile-Friendly Fix */
@media (max-width: 800px) {
    button {
        font-size: 1rem;
        padding: 14px;
        width: 100%;
        max-width: 250px;
    }




    .button-grid {
        grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); /* Adjusts for smaller screens */
    }
}
    </style>
    <script>








function compensateUp() {
    fetch('/compensateUp')
      .then(response => response.text())
      .then(data => console.log(data))  // Log the response
      .catch(error => console.error('Error:', error));
}




function compensateDown() {
    fetch('/compensateDown')
      .then(response => response.text())
      .then(data => console.log(data))  // Log the response
      .catch(error => console.error('Error:', error));
}




function autoTare() {
    fetch('/autoTare')
      .then(response => response.text())
      .then(data => console.log(data))  // Log the response
      .catch(error => console.error('Error:', error));
}




function syncNTP() {
    fetch('/syncNTP')
      .then(response => response.text())
      .then(data => console.log(data))  // Log the response
      .catch(error => console.error('Error:', error));
}




function microDOWN() {
    fetch('/microDOWN')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function microUP() {
    fetch('/microUP')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function timeAdjustINC() {
    fetch('/timeAdjustINC')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function timeAdjustDEC() {
    fetch('/timeAdjustDEC')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function bufferReset() {
    fetch('/bufferReset')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function neverPress() {
    fetch('/neverPress')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function lockEEPROM() {
    fetch('/lockEEPROM')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




function modeSelect() {
    fetch('/modeSelect')
      .then(response => response.text())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
}




        function updateVariables() {
            fetch('/variables')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('unixTime').innerText = data.unixTime;
                    document.getElementById('nistTime').innerText = data.nistTime;
                    document.getElementById('date').innerText = data.date;
                    document.getElementById('time').innerText = data.time;
                    document.getElementById('displayMicroSeconds').innerText = data.displayMicroSeconds;
                    document.getElementById('microChange').innerText = data.microChange;
                    document.getElementById('pulseDurationBetweenRisingEdges').innerText = data.pulseDurationBetweenRisingEdges;
                    document.getElementById('timeAdjust').innerText = data.timeAdjust;
                    document.getElementById('differenceTime').innerText = data.differenceTime;
                    document.getElementById('freeMemory').innerText = data.freeMemory;
                    document.getElementById('averagePDBRE').innerText = data.averagePDBRE;
                    document.getElementById('runtimeSeconds').innerText = data.runtimeSeconds;
                    document.getElementById('totalPDBRE').innerText = data.totalPDBRE;
                    document.getElementById('adjustmentsMade').innerText = data.adjustmentsMade;
                    document.getElementById('xtalFreq').innerText = data.xtalFreq;
                    document.getElementById('avgXtalFreq').innerText = data.avgXtalFreq;
                    document.getElementById('fault').innerText = data.fault;
                    document.getElementById('globalPulseCount').innerText = data.globalPulseCount;
                    document.getElementById('espRuntime').innerText = data.espRuntime;
                    document.getElementById('lastNistUpdate').innerText = data.lastNistUpdate;
                    document.getElementById('runtimeHours').innerText = data.runtimeHours;
                    document.getElementById('faultCode').innerText = data.faultCode;
                    document.getElementById('bootupTime').innerText = data.bootupTime;
                    document.getElementById('status').innerText = data.status;
                    document.getElementById('stackSize').innerText = data.stackSize;
                    document.getElementById('ntpRequests').innerText = data.ntpRequests;
                    document.getElementById('nistAdjuster').innerText = data.nistAdjuster;
                    document.getElementById('compensateVariable').innerText = data.compensateVariable;
                    document.getElementById('oneWayDelay').innerText = data.oneWayDelay;
                    document.getElementById('mode').innerText = data.mode;
                    document.getElementById('totalAdjustedFaultTime').innerText = data.totalAdjustedFaultTime;
                    document.getElementById('runtimeDays').innerText = data.runtimeDays;
                    document.getElementById('nextDatum').innerText = data.nextDatum;
            
                });
        }
        setInterval(updateVariables, 250);
    </script>


  <div class="container">
    <div class="header">
          EACP<br>
        <span class="header2">(ElectroKev Atomic Clock Project)</span>
    </div>
    
    <div class="subheader">Developed by <strong>Kevin Davy</strong> | January 2025</div> 
    
    <div class="version-container">
        <span id="version" class="version">Loading version...</span>
        <span id="lastUpdated" class="last-updated">
            Last Code Upload: )rawliteral" + String(buildDate) + " at " + String(buildTime) +  R"rawliteral( 
        </span>
    </div>




    <div class="ntp-container">
        <span class="version">NTP Server:</span>
        <span id="ntp" class="ntp">clock.electrokev.com Port 123</span> 
        <span class="status-label">Status:</span>
        <span id="status" class="status">-</span>
    </div>




    <div class="bootup-container">
    Bootup Time: <span id="bootupTime"></span> 
    MODE: <span id="mode" style="color: red;"></span>
</div>




    <script>
        function updateVersion() {
            fetch('/getVersion')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('version').innerText = "Code Version " + data.version;
                });
        }
        updateVersion();
    </script>
</div>





        <div class="variables">
            <div class="variable" style="color: turquoise;background: Teal;"><strong>EACP UNIX:<br></strong> <span id="unixTime">-</span></div>
            <div class="variable" style="color: White;background: Teal;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
            <div class="variable" style="color: yellow;background: Teal;"><strong>Stratum1 UNIX:<br></strong> <span id="nistTime">-</span></div>
            <div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
            <div class="variable" style="color: orange;background: Teal;"><strong>Datum / Next :<br></strong> <span id="lastNistUpdate">-</span> / <span id="nextDatum">-</span></div>
            <div class="variable"><strong>NTP Requests:<br></strong> <span id="ntpRequests">-</span></div>
            <div class="variable" style="background: DarkOliveGreen;"><strong>Pulse Comp:<br></strong> <span id="nistAdjuster">-</span> s</div>
            <div class="variable" style="background: DarkOliveGreen;"><strong>Tare:<br></strong> <span id="compensateVariable">-</span> s</div>
            <div class="variable" style="background: DarkOliveGreen;"><strong>Offset (Seconds):<br></strong> <span id="differenceTime">-</span> s</div>
            <div class="variable" style="background: SteelBlue;"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
            <div class="variable" style="background: SteelBlue;"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
            <div class="variable" style="background: SteelBlue;"><strong>xTal Faults/Time:<br></strong> <span id="fault">-</span> / <span id="totalAdjustedFaultTime">-</span> s</div>
            <div class="variable" style="background: Olive;"><strong>PDBRE:<br></strong> <span id="pulseDurationBetweenRisingEdges">-</span> μS</div>
            <div class="variable" style="background: Olive;"><strong>Avg PDBRE:<br></strong> <span id="averagePDBRE">-</span> μs</div>
            <div class="variable" style="background: Olive;"><strong>PDBRE Err:<br></strong> <span id="totalPDBRE">-</span> μs</div>
            <div class="variable" style="background: Sienna;"><strong>Adjust:<br></strong> <span id="timeAdjust">-</span> s/Day</div>
            <div class="variable" style="background: Sienna;"><strong>μs/Pulse+-:<br></strong> <span id="microChange">-</span></div>
            <div class="variable" style="background: Sienna;"><strong>PulseAdj/Next:<br></strong>  <span id="adjustmentsMade">-</span> / <span id="displayMicroSeconds">-</span> Hrs</div>
            <div class="variable"><strong>RunTime (S:H:D)<br></strong> <span id="runtimeSeconds">-</span> / <span id="runtimeHours">-</span> / <span id="runtimeDays">-</span></div>
            <div class="variable"><strong>ESP RunTime<br></strong> <span id="espRuntime">-</span></div>
            <div class="variable"><strong>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
            <div class="variable"><strong>NTP Stack Size:<br></strong> <span id="stackSize">-</span> bytes</div>
            <div class="variable"><strong>Network Delay:<br></strong> <span id="oneWayDelay">-</span> s</div>
            <div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> bytes</div>
            <span id="faultCode" style="display: none;">-</span>
        </div>
    




  <div class="serial-container"> Serial Output Console
    <div id="serialOutput" class="serial-output"></div> 
  </div>




  <script>
    function addSerialMessage(message) {
     
    document.getElementById('serialOutput').innerHTML = ''; // Clears the content
    const serialOutput = document.getElementById('serialOutput');
    const currentTime = new Date().toLocaleTimeString();
    const faultCode = document.getElementById('faultCode').innerText; // Get faultCode
    const newMessage = `<div> <span class="timestamp">UTC: [${currentTime}]</span><br>${message}<strong> ${faultCode} </strong></div>`; 
    serialOutput.innerHTML += newMessage;




    // Scroll to the bottom
    serialOutput.scrollTop = serialOutput.scrollHeight;
}




    
//trigger the function every 2 seconds to update the console
    setInterval(() => {
      addSerialMessage(" ");
    }, 2000); // Every 2 seconds, add a new message
</script>








      
<div class="description">
<div class="admin-controls">
  <p>Administrator Controls - Auth Use Only</p>
  
  
  <div id="unlock-section">
    <input type="password" id="admin-password" placeholder="Enter Password">
    <button onclick="unlockControls()">Unlock Controls</button>
  </div>




  <div id="button-section" style="margin-top:20px;">
    <div class="button-grid">
      <button onclick="syncNTP()" disabled="">Master Sync</button>
      <button onclick="microUP()" disabled="">Pulse Inject</button>
      <button onclick="timeAdjustINC()" disabled="">mSADJDAYINC</button>
      <button onclick="compensateDown()" disabled="">+ve Tare</button>
      <button onclick="bufferReset()" disabled="">Fault Reset</button>
      <button onclick="microDOWN()" disabled="">Pulse Extract</button>
      <button onclick="timeAdjustDEC()" disabled="">mSADJDayDEC</button>
      <button onclick="compensateUp()" disabled="">-ve Tare</button>
      <button onclick="modeSelect()" disabled="">Mode Select</button>
      <button onclick="lockEEPROM()" disabled="">NTP/Save</button>
      <button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;"  ="" onclick="confirmAction('Restart All Services', neverPress)" disabled="">
        Restart All Services
      </button>
      <button onclick="autoTare()" disabled="">Auto Tare</button>
    </div>
  </div>
</div>




<script>
  
  // Original password-protected function (commented out)
  //start comment here




  async function hashPassword(password) {
    const encoder = new TextEncoder();
    const data = encoder.encode(password);
    const hashBuffer = await crypto.subtle.digest("SHA-256", data);
    return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
}




// Precomputed hash of password
const storedHash = "8254c329a92850f6d539dd376f4816ee2764517da5e0235514af433164480d7a";  




async function unlockControls() {
    const enteredPassword = document.getElementById("admin-password").value;
    const enteredHash = await hashPassword(enteredPassword);




    if (enteredHash === storedHash) {
        const buttons = document.querySelectorAll("#button-section button");
        buttons.forEach(button => {
            button.disabled = false;
        });
        document.getElementById("unlock-section").style.display = "none";  
    } else {
        alert("Password Incorrect");
    }
} //end of commented out section 
  
  /*
//start comment to unlock without password
  // New function to unlock controls without a password
  function unlockControls() {
    // Enable all buttons without requiring a password
    const buttons = document.querySelectorAll("#button-section button");
    buttons.forEach(button => {
      button.disabled = false;
    });
    
  }
  




  // Automatically unlock controls on page load 
   window.onload = unlockControls;
// end comment 
*/




  
  // Confirmation action function
  function confirmAction(action, callback) {
    if (confirm("Are you sure you want to " + action + "?")) {
      callback();
    }
  }
</script>








</div>
<div class="description">
  <h3>ElectroKev Atomic Clock Features</h3>
  <div class="mainlinks">
  <a href="https://electrokev.com/eacp-code-v53.8" target="_blank">[22/02/25] ** Source Code View ** v53.8</a><br>
  <a href="https://electrokev.com/eacp-code-v54.1" target="_blank">[02/03/25] ** Source Code View ** v54.1</a><br>
  <a href="https://electrokev.com/eacp-code-v54.3" target="_blank">[12/03/25] ** Source Code View ** v54.3</a>
  <span style="color: red;"> * Latest Version *</span>
<p>
</p>
<iframe id="myIFrame" src="https://electrokev.com/wp-content/uploads/html/manual1.html"  ="" loading="lazy" width="100%" style="border:none;"></iframe>




<script>
  window.addEventListener("message", (event) => {
    // Ensure the message is from the correct domain
    if (event.origin !== "https://electrokev.com") return;




    if (event.data.iframeHeight) {
      document.getElementById("myIFrame").style.height = event.data.iframeHeight + "px";
    }
  });
</script>
</div>








</div>





<div class="footer">
  <a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
  © Kevin Davy 2025
</div>
    <br><br> Last Code Upload: )rawliteral"
                + String(buildDate) + " at " + String(buildTime) + R"rawliteral(


<p></p>

)rawliteral";




  request->send(200, "text/html", index_html);
}</wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></espasyncwebserver.h></wifi.h></unixtime.h></liquidcrystal_i2c.h></wire.h></secrets.h>