Close
Skip to content

EACP Code v50.1

/****************************************************************************
 * ElectroKev Atomic Clock  
 * Author: Kevin Davy ([email protected])  
 * © 2025 - All Rights Reserved  
 *
 * 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 1
   RUN ARDUINO ON CORE 1 //changed to core 0 due to Watchdog failure
   RUN PULSEPROCESSBUFFER ON CORE 0
   RUN ISR ON {PROCESSOR DECIDES BUT SET TO 1} */








const float versionNumber = 51.0;




#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <eeprom.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include "freertos/task.h"
#include "esp_bt.h"
#include <esp_system.h>
#include <esp_task_wdt.h>
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.h>
#include <wifiudp.h>
#include <ticker.h>
//#include "driver/gpio.h"




//network stuff
const char *ssid = SSID;
const char *password = PASSWORD;
const char *buildDate = __DATE__;
const char *buildTime = __TIME__;




// Set the static IP details
IPAddress local_ip(192, 168, 1, 172);  //local ip
IPAddress gateway(192, 168, 1, 1);     // gatewat
IPAddress subnet(255, 255, 255, 0);    // subnet




AsyncWebServer server(8080);
Preferences preferences;




// --- NTP Configuration ---
const char *ntpServer = "pool.ntp.org";  // NTP server
const long gmtOffset_sec = 0;            // Offset for UTC
const int daylightOffset_sec = 0;        // No daylight savings time
WiFiUDP ntpUDP;
const int NTP_PORT =123;




// --- Pin Configurations ---
#define ROTARY_CLK 26             // Rotary encoder clock pin
#define ROTARY_DT 27              // Rotary encoder data pin
#define ROTARY_SW 13              // Rotary encoder switch pin
#define adjustHoursPin 16         // Button pin for adjusting hours
#define adjustMinutesPin 17       // Button pin for adjusting minutes
#define adjustSecondsPin 5        // Button pin for adjusting seconds
#define adjustMicroSecondUp 18    // Button pin for increasing microseconds
#define adjustMicroSecondDown 19  // Button pin for decreasing microseconds
#define syncNTC 23
#define pulsePin 12  // Pulse pin input for counting pulses
#define BUFFER_SIZE 256








// --- Global Objects ---
UnixTime stamp(0);
LiquidCrystal_I2C lcd(0x27, 20, 4);




// --- Time Variables ---
double microSeconds = 0;
double displayMicroSeconds;
double microChange = 0;
float rotaryChangeValue = 0.1;  // Rotary encoder adjustment step
float timeAdjust = 0;
double pulseDurationBetweenRisingEdges;  //changed from float
double invalidPulse;
double averagePDBRE;                     //changedfrom float
double totalPDBRE;     
double totalPDBREAC=100000;         //changed from float
long long espRuntime=0; //long to double
long runtimeSeconds = 0; //long to double
long runtimeHours=0;
const int crystalCorrection = 0;  // Crystal correction factor
int adjustmentsMade;
double xtalFreq; //float to double
double totalXtalFreq; //float to double
double avgXtalFreq; //float to double
int lastUpdate = 0;
String timeNow;




//fault variables
int fault = 0;
int faultTime;
bool generalFault=true;
String status="FAULT";
String faultCode ="Fault: Unexpected ReStart ";
double faultyPDBRE;
bool flagInvalidPulse;
bool flagTimeDrift;
bool flagUnexpectedRestart=true;
bool flagNTPReceived;
bool flagNTPSync;
bool flagVisitor;
bool flagStackSize;












// --- Circular Buffer Variables ---
//volatile double currentTime;  
volatile double currentTime; // uint to double
volatile double nextHead; // uint to double
volatile double pulseTimes[BUFFER_SIZE]; // was double then long now int
volatile uint32_t bufferHead = 0;
volatile uint32_t bufferTail = 0;
double lastRisingEdgeTime = 0;
int pulseCount = 0;
double globalPulseCount = 0;




// --- Date Setting  ---
bool dateSetting = 0;




// --- Time Variables ---
int32_t nistTime;
int32_t unix;
double differenceTime; //int to double
double lastNistUpdate;
double bootupTime;




// --- General Variables ---
int visitorsCount;
int freeMemory = 0;
String clientIP;
double stackSize;








// --- Interrupt Service Routine (ISR) ---
void IRAM_ATTR pulseISR() {
     /* int coreID = xPortGetCoreID();  // Returns 0 for Core 0, 1 for Core 1
    Serial.print("ISR on Core: ");
    Serial.println(coreID);*/
 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() {
  //Shut down BT




  Serial.begin(115200);




  if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
    esp_bt_controller_disable();
    esp_bt_controller_deinit();
    Serial.println("Bluetooth disabled.");
  } else {
    Serial.println("Bluetooth was already disabled.");
  }












  // Setup WiFi connection
  setupWifi();  // Starts WiFi and web server (which runs on Core 0)
  freeMemory = ESP.getFreeHeap();
  Serial.println("Starting OTA...");
  ArduinoOTA.begin();
  Serial.println("OTA Ready!");
  ArduinoOTA.setPassword(nullptr);  // Disable password




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




  // --- Pulse Pin Setup ---
  pinMode(pulsePin, INPUT);








  // --- 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(ROTARY_SW, INPUT_PULLUP);
  pinMode(syncNTC, INPUT_PULLUP);




  loadFromEEPROM();




  // --- Set Date and Time from Unix Timestamp ---
  syncWithNTP();
  stamp.getDateTime(unix);








attachInterrupt(digitalPinToInterrupt(pulsePin), pulseISR, FALLING);








  // Create the task for processing the pulse buffer
  xTaskCreatePinnedToCore(
    [](void*) {
      while (true) {
        processPulseBuffer();  // This function runs in a separate task
        vTaskDelay(15);  // Delay to give CPU time to other tasks, adjust if needed
      }
    },
    "processPulseBuffer",  // Task name
    4096,  // Stack size (adjust if needed)
    NULL,  // No parameters
    4,  // Priority (set according to your needs)
    NULL,  // Task handle
    0  // Pin to Core 0 (or Core 0 depending on your setup)
  );








    xTaskCreatePinnedToCore(
    [](void*){
      pulseISR();
      vTaskDelete(NULL);  // Delete task when done
    },
    "pulseISR",  // Task name
    4096,  // Stack size (adjust if needed)
    NULL,  // No parameters
    10,  // Priority (set according to your needs)
    NULL,  // Task handle
    1  // Pin to Core 0
  );












  Serial.println("Running");
  flagUnexpectedRestart=true;
  faultHandler();
}




// --- Main Loop ---
void loop() {
  //processPulseBuffer();   // Process pulses from the buffer
  //handleButtons();        // Check and process button inputs
  //handleRotaryEncoder();  // Handle rotary encoder inputs
  adjustmentRoutine();    // Adjust time if microseconds overflow
  ArduinoOTA.handle();
  processNTPRequests();
  faultCheck();
  vTaskDelay(10);
}








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




void faultHandler() {
    generalFault = false;  // Reset before checking flags




    // 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";
        generalFault = true;
    }




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




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




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




IRAM_ATTR void processPulseBuffer() {
   //int coreID = xPortGetCoreID();  // Returns 0 for Core 0, 1 for Core 1
   // Serial.print("Pulse Processor on Core: ");
   // Serial.println(coreID);
    static unsigned long lastMillis = millis();  // Track last update time for nistTime




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




        pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime) + crystalCorrection;
        lastRisingEdgeTime = pulseTime;




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




        // Validate the pulse duration
        if (pulseDurationBetweenRisingEdges >= 50000 && pulseDurationBetweenRisingEdges <= 150000) {
            pulseCount++;
        } else {
            invalidPulse=pulseDurationBetweenRisingEdges;
            fault++;
            flagInvalidPulse = true;
            faultHandler();
        }




        // Calculate crystal frequency
        xtalFreq = 1000000 / pulseDurationBetweenRisingEdges;




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




        globalPulseCount++;




        // Perform 1-minute tasks
        if (espRuntime % 60 == 0) {
            nistUpdate();
            lastNistUpdate = nistTime;
        }




        // Reset after 1 hour
        if (runtimeSeconds >= 3600) {
          if(stackSize<2000){
            flagStackSize=true;
            faultHandler();
          }
            totalPDBREAC=0;
            totalPDBRE = 100000;
            runtimeSeconds = 0;
            runtimeHours++;
            averagePDBRE = 0;
            avgXtalFreq = 0;
            totalXtalFreq = 0;
            globalPulseCount = 0;
            freeMemory = ESP.getFreeHeap();
        }
//displayVariables();
        // Process accumulated pulses (every 10 pulses)
        if (pulseCount >= 10) {
            if (runtimeSeconds > 0) {
                totalPDBREAC += pulseDurationBetweenRisingEdges;  // Update total pulse duration
                totalPDBRE = ((runtimeSeconds + 1) * 100000) - totalPDBREAC;
                averagePDBRE = totalPDBREAC / (runtimeSeconds + 1);  // Long-term average pulse duration
            }




            runtimeSeconds++;
            pulseCount = 0;
            microSeconds += (microChange * 10);  // Adjust microseconds based on pulse count




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




            // Update time and display once per successful 10-pulse cycle
            updateTime();
            updateDisplay();
        }




        // Increment `nistTime` every 1000 milliseconds
        if (millis() - lastMillis >= 1000) {
            differenceTime = unix - nistTime;
            nistTime++;
           stackSize=(uxTaskGetStackHighWaterMark(NULL));




            // Set fault code if time sync drifts beyond limit
            if (differenceTime >= 3 || differenceTime <= -3) {
                flagTimeDrift = true;
                faultHandler();
                syncWithNTP();
            }




            lastMillis += 1000;
            espRuntime++;
        }
    }




}








void processNTPRequests() {
    int packetSize = ntpUDP.parsePacket();
    if (packetSize) {
        Serial.println("NTP request received");
        byte packetBuffer[48]; // NTP request packet
        ntpUDP.read(packetBuffer, packetSize); // Read request




        // Set up the NTP response structure
        memset(packetBuffer, 0, 48);
        packetBuffer[0] = 0b00100100; // LI, Version, Mode




        // Get current Unix time from your OCXO-based clock
        unsigned long currentEpoch = unix;




        // Convert to NTP timestamp format (seconds since 1900)
        unsigned long ntpSeconds = currentEpoch + 2208988800UL;
        packetBuffer[40] = (ntpSeconds >> 24) & 0xFF;
        packetBuffer[41] = (ntpSeconds >> 16) & 0xFF;
        packetBuffer[42] = (ntpSeconds >> 8) & 0xFF;
        packetBuffer[43] = ntpSeconds & 0xFF;




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




        Serial.println("NTP response sent");
        flagNTPReceived=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);
        }




        delay(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++;
  stamp.getDateTime(unix);
  timeNow = "["+String(formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second))+"]";
}




// --- 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),0;  // Display Unix timestamp
  lcd.setCursor(0, 2);
  lcd.print("NTP: ");
  lcd.print(nistTime),0;




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




  // Display pulse adjustments
  lcd.setCursor(14, 3);
  lcd.print("PA:");
  lcd.print(adjustmentsMade);
}




// --- Adjustment Routine ---
void adjustmentRoutine() {
  if (microSeconds >= 100000) {
    microSeconds = 0;
    pulseCount++;
    adjustmentsMade++;
  } else if (microSeconds <= -100000) {
    microSeconds = 0;
    pulseCount--;
    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);
  //unix = preferences.getLong("unix", 0);
  //visitorsCount = preferences.getInt("visitorsCount", 0);
  runtimeHours = preferences.getDouble("runtimeHours", 0);
  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.putLong("unix", unix);
  //preferences.putInt("visitorsCount", visitorsCount);
  preferences.putDouble("runtimeHours", runtimeHours);
  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);




  // Wait for the WiFi connection
  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());
  ntpUDP.begin(NTP_PORT);
  Serial.println("NTP Server started on port 123");
  // Set up route handlers
  routeHandlers();




  // Start the server
  server.begin();
  WiFi.setSleep(false);




  // Server started message
  Serial.println("Server started!");
}








void handleVariables(AsyncWebServerRequest *request) {
  // Create a JSON document (this is a fixed-size buffer to hold the JSON)
  StaticJsonDocument<1024> doc;




  // Fill the JSON document with data
  doc["unixTime"] = unix;
  doc["bootupTime"] = bootupTime;
  doc["nistTime"] = nistTime;
  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"] = differenceTime;
  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"] = lastNistUpdate;
  doc["runtimeHours"] = runtimeHours;
  doc["faultCode"] = String(faultCode);
  doc["status"] = String(status);




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




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




void nistUpdate() {
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  struct tm timeinfo2;
  if (!getLocalTime(&timeinfo2)) {
    return;
  }
  nistTime = (mktime(&timeinfo2));
}




void syncWithNTP() {
  visitorsCount = 0;




  // Configure NTP
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  Serial.println("Connecting to NTP server...");




  // Wait for NTP sync
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo, 10000)) {  // Wait for up to 10 seconds
    Serial.println("Failed to get time from NTP server.");
    lcd.clear();
    lcd.print("NTP Sync Failed!");
    return;
  }




  // Convert to Unix timestamp and update time
  //unix = (mktime(&timeinfo))+ 1;
  unix = (mktime(&timeinfo));
  bootupTime=unix;
  nistTime = (mktime(&timeinfo));
  microSeconds = 0;  // Reset microsecond adjustments
  pulseCount = 0;
  stamp.getDateTime(unix);




  // Debug Output
  Serial.println("NTP Sync Successful!");
  flagNTPSync=true;
  faultHandler();
  Serial.print("Unix Time: ");
  Serial.println(unix);




  // Update LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("NTP Sync OK!");
  lcd.setCursor(0, 1);
  lcd.print("Time Updated.");
  
}




void fullReset(){
    generalFault=false;
    fault = 0;
    flagInvalidPulse=false;
    flagUnexpectedRestart=false;
    flagTimeDrift=false;
    flagNTPReceived=false;
    flagNTPSync=false;
    flagVisitor=false;
    flagStackSize=false;
    faultCode=" Status: OK : StackSize = "+String(stackSize)+"\n";
    totalPDBRE = 0;
    totalPDBREAC=100000;
    runtimeSeconds = 0;
    espRuntime =0;
    averagePDBRE = 0;
    xtalFreq = 0;
    avgXtalFreq = 0;
    totalXtalFreq = 0;
    globalPulseCount = 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++;
    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) {
    pulseCount--;
    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
  server.on("/lockEEPROM", HTTP_GET, [](AsyncWebServerRequest *request) {
    saveToEEPROM();
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("EEPROM Saved");
    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");
  });
}




void handleRoot(AsyncWebServerRequest *request) {
  // Increment visitor count on each webpage load
  clientIP=request->client()->remoteIP().toString();
  visitorsCount++;
  flagVisitor=true;
  faultHandler();




  String 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; }
}








/* -------------------------------------------- */
/* Modern Button Styling - Editable Section */
/* -------------------------------------------- */




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 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 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;
                });
        }
        setInterval(updateVariables, 500);
    </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></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>NIST 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>NIST Update:<br></strong> <span id="lastNistUpdate">-</span></div>
            <div class="variable"><strong>Accuracy:<br></strong> <span id="differenceTime">-</span> seconds</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:<br></strong> <span id="fault">-</span></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 (Sec/Hrs)<br></strong> <span id="runtimeSeconds">-</span> / <span id="runtimeHours">-</span></div>
            <div class="variable"><strong>ESP RunTime<br></strong> <span id="espRuntime">-</span></div>
            <div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> bytes</div>
            <div class="variable"></div>
            <div class="variable"><strong>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
            <div class="variable"></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">[${currentTime}]</span> ${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="">Sync with NTP</button>
      <button onclick="microUP()" disabled="">microUP</button>
      <button onclick="timeAdjustINC()" disabled="">mSADJDAYINC</button>
      <button onclick="lockEEPROM()" disabled="">EEPROM</button>
      <button onclick="bufferReset()" disabled="">Fault Reset</button>
      <button onclick="microDOWN()" disabled="">microDOWN</button>
      <button onclick="timeAdjustDEC()" disabled="">mSADJDayDEC</button>
      <button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;"  ="" onclick="confirmAction('Never Press this Button!', neverPress)" disabled="">
        NEVER Press this Button
      </button>
    </div>
  </div>
</div>




<script>
  /* 
  // Original password-protected function (commented out)
  const correctPassword = "kev"; 




  function unlockControls() {
    const enteredPassword = document.getElementById("admin-password").value;
    
    if (enteredPassword === correctPassword) {
      // Enable all buttons after password is correct
      const buttons = document.querySelectorAll("#button-section button");
      buttons.forEach(button => {
        button.disabled = false;
      });
      document.getElementById("unlock-section").style.display = "none";  // Hide the password input
    } else {
      alert("Password Incorrect");
    }
  }
  */




  // 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 (optional)
  window.onload = unlockControls;








  
  // 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-v49-6/" target="_blank">[24/01/25] ** Source Code View ** v49.6</a><br>
  <a href="https://electrokev.com/eacp-code-v50-3/" target="_blank">[28/01/25] ** Source Code View ** v50.3</a><br>
  <a href="https://electrokev.com/eacp-code-v50-6/" target="_blank">[29/01/25] ** Source Code View ** v50.6</a>
  <span style="color: red;"> * Latest Version *</span>
  </div>
  <p>The <strong>ElectroKev Atomic Clock</strong> incorporates advanced pulse monitoring and modulation technology, with precise synchronization and real-time correction of the time signal, ensuring microsecond-level accuracy. By tracking pulse drift from an oven-controlled OCXO 10 MHz Crystal, the clock dynamically compensates for drift, maintaining highly accurate timekeeping over extended periods.</p>
  <h4>Key Features:</h4>
  <ul>
    <li><strong>Advanced Pulse Monitoring:</strong> The 10 MHz crystal is scaled by factors of 10 using 6 layers of the 74LS90 integrated circuit, creating a final 10 Hz input. The system continuously monitors and adjusts pulse signals to ensure precise timing.</li>
    <li><strong>Real-Time Drift Compensation:</strong> Compensates for long-term drift with microsecond-level accuracy, ensuring stable timekeeping.</li>
    <li><strong>User-Controlled Adjustments:</strong> Allows fine-tuning of pulse skipping and drift compensation to achieve optimal accuracy.</li>
    <li><strong>Multi-LCD Display System:</strong> Provides detailed data on time, pulse handling, drift calculations, and temperature.</li>
    <li><strong>Temperature Monitoring:</strong> Offers real-time temperature data with high precision and alerts for sensor malfunctions.</li>
    <li><strong>Output Signal Control:</strong> Generates a modulated output signal with precise timing adjustments for synchronization.</li>
    <li><strong>Rotary Encoder Integration:</strong> Enables precise adjustments to pulse modulation intervals with high resolution.</li>
    <li><strong>Modular Design:</strong> Easily customizable to integrate additional sensors and outputs, enhancing functionality.</li>
    <li><strong>Twin stabilised power supplies:</strong> The system uses twin power supplies, both stabilised and voltage and current regulated for accurate crystal oscillation.</li>
</ul>
<p>
Further Details: 
</p>
<p>
  The clock is an advanced, pulse-modulated timekeeping system that achieves exceptional precision by synchronizing with an external oven controlled crystal outputting a 10 MHz pulse. This system is designed to manipulate pulses with a remarkable accuracy of up to 1 microsecond per 24 hours, ensuring precise timekeeping over extended periods. It uses a segregated NIST time reference for synchronization, allowing it to maintain accurate time based on trusted atomic standards.
</p>




<p>
  For manual adjustments, the clock offers controls to modify the time and date, allowing for fine-tuning through pulse injection or removal. A rotary encoder is used to adjust the daily time alteration rate, with the system automatically calculating the microseconds per pulse to ensure precision. This feature makes it adaptable for environments requiring high-precision time adjustments.
</p>




<p>
  The clock is equipped with WiFi connectivity, enabling remote monitoring and control through a web application. This interface provides full access to all major clock functions, allowing users to interact with the system easily via server handlers. Continuous crystal speed monitoring helps identify and debug drift, ensuring the clock remains highly accurate.
</p>




<p>
  In terms of fault detection, the clock is designed to identify invalid pulse readings automatically. It removes faulty pulses and logs debug data for analysis, making it resilient to errors. Additionally, the power supply is finely tuned, with regulated power sources dedicated to different components to ensure stable performance.
</p>




<p>
  Firmware updates are handled over the air (OTA), ensuring the clock remains up-to-date without requiring manual intervention. The clock operates with a dual-core configuration, separating timekeeping functions from resource-intensive processes to optimize performance. Pulse monitoring inputs are prioritized through high-level processor interrupts for precise counting.
</p>




<p>
  The clock also functions as a globally accessible NTP time server on port 123, providing highly accurate time synchronization. A web-based serial console is available for real-time debugging, with self-monitoring and error reporting capabilities to facilitate automatic correction of any issues.
</p>




</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", html);
}</ticker.h></wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></esp_task_wdt.h></esp_system.h></espasyncwebserver.h></wifi.h></unixtime.h></eeprom.h></liquidcrystal_i2c.h></wire.h></secrets.h>