Close
Skip to content

EACP Code v50.3

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








const float versionNumber = 50.3;




#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>




//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, 208);  //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 34  // Pulse pin input for counting pulses
#define BUFFER_SIZE 16








// --- 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 averagePDBRE;                     //changedfrom float
double totalPDBRE = 100000;              //changed from float
long espRuntime=0;
long runtimeSeconds = 0;
long runtimeHours=0;
const int crystalCorrection = 2;  // Crystal correction factor
int adjustmentsMade;
float xtalFreq;
float totalXtalFreq;
float avgXtalFreq;
int lastUpdate = 0;




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












// --- Circular Buffer Variables ---
volatile double currentTime;  
volatile double pulseTimes[BUFFER_SIZE];
volatile int bufferHead = 0;
volatile int bufferTail = 0;
double lastRisingEdgeTime = 0;
int pulseCount = 0;
double globalPulseCount = 0;




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




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




// --- General Variables ---
int visitorsCount;
int freeMemory = 0;












// --- Interrupt Service Routine (ISR) ---
void IRAM_ATTR pulseISR() {
  currentTime = esp_timer_get_time();
  int 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);








  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();
}




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




void faultHandler() {
    generalFault = false;  // Reset generalFault before checking flags
if (flagUnexpectedRestart) {
    // Get the reset reason
    esp_reset_reason_t resetReason = esp_reset_reason();
    
    // Convert the reset reason to a string
    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;
    }




    // Append the faultCode with the reset reason and the timestamp
    faultCode += "Fault: Unexpected Restart EACP_RST" + resetReasonText + " " + String(unix) + "\n";
    generalFault = true;
}
    if (flagTimeDrift) {
        faultCode += " Fault: Time Drift " + String(unix)+ " "+differenceTime+ "\n";
        generalFault = true;
    }
    if (flagInvalidPulse) {
        faultCode += " Fault: Invalid Pulse "+String(unix)+ " " + "("+pulseDurationBetweenRisingEdges+")"+"\n";
        generalFault = true;
    }




     if (flagNTPReceived) {
        faultCode += " NTP Request Recieved "+String(unix)+"\n";
        flagNTPReceived=false;
    }   
     if (flagNTPSync) {
        faultCode += " NTP Successfully Synced "+String(unix)+"\n";
        flagNTPSync=false;
    }   
    // If there are faults, remove the trailing comma and space
   /* if (!generalFault) {
        faultCode = "Status: OK ";  // No faults detected
    }*/
    saveToEEPROM();
    status = generalFault ? "FAULT" : "OK";  // Update status
}








void processPulseBuffer() {
  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;




    // Wait for variables to stabilize
    if (runtimeSeconds == 0) {
      globalPulseCount = 9;
      totalXtalFreq = 90;
      fault = 0;
    }




    // Process the pulse
    if (pulseDurationBetweenRisingEdges >= 99980 && pulseDurationBetweenRisingEdges <= 100020) {
      pulseCount++;
    } else {
        fault++;
        flagInvalidPulse=true;
        faultHandler();
    }




    // Calculate crystal frequency
    //xtalFreq = ((10000000 * (100000 / pulseDurationBetweenRisingEdges))) / 1000000;
    xtalFreq=1000000/pulseDurationBetweenRisingEdges;




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




    //displayVariables();
    globalPulseCount++;




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




    // Reset after 1 hour
    if (runtimeSeconds >= 3600) {
      totalPDBRE = 100000;
      runtimeSeconds = 0;
      runtimeHours++;
      averagePDBRE = 0;
      xtalFreq = 0;
      avgXtalFreq = 0;
      totalXtalFreq = 0;
      globalPulseCount = 0;
    }




    // Process accumulated pulses
    if (pulseCount >= 10) {
      if (runtimeSeconds > 0) {
        totalPDBRE += pulseDurationBetweenRisingEdges;  // Update total pulse duration
        averagePDBRE = totalPDBRE / (runtimeSeconds + 1);  // Long-term average pulse duration
      }
      runtimeSeconds++;
      pulseCount = 0;
      microSeconds += (microChange * 10);  // Add 10x microChange as ten pulses per second




      // Adjust display so remaining microseconds are shown in hours between pulse adjustments
      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);  // Calculate microseconds per day








    // 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 based on ESP32 crystal
    if (millis() - lastMillis >= 1000) {
      nistTime++;
      differenceTime = (unix - nistTime);
      if (differenceTime >=3 | differenceTime <=-3){ //set fault code if sync drifts beyond limit
        flagTimeDrift=true;
        faultHandler();
      }
      lastMillis += 1000;
      espRuntime++;
      if (espRuntime>=3600){
      }
    }
  }
}








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




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




  // 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;
  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;
    faultCode=" Status: OK \n";
    totalPDBRE = 100000;
    runtimeSeconds = 0;
    espRuntime =0;
    averagePDBRE = 0;
    xtalFreq = 0;
    avgXtalFreq = 0;
    totalXtalFreq = 0;
    globalPulseCount = 0;
    faultCode=("Status: OK");
    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
  visitorsCount++;




  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: 16px;
    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;
    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: 2rem; }
    .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, 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></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>Total PDBRE<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> / 3600</div>
            <div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> KB</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>
  // password for control panel
  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");
    }
  }
  
  // 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-0/" target="_blank">** Source Code View ** v49.0</a><br>
  <a href="https://electrokev.com/eacp-code-v49-5/" target="_blank">** Source Code View ** v49.5</a><br>
  <a href="https://electrokev.com/eacp-code-v49-6/" target="_blank">** Source Code View ** v49.6</a><br>
  <a href="https://electrokev.com/eacp-code-v50-3/" target="_blank">** Source Code View ** v50.3</a>
  <span style="color: red;"> ** Latest Stable 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);
}</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>