Close
Skip to content

EACP Code v49.2

/****************************************************************************
 * ElectroKev Atomic Clock - Version 49.2
 * Author: Kevin Davy ([email protected])
 * © 2025 - All Rights Resered 
 *
 * 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.
 *
 *
 ***************************************************************************/








const float versionNumber = 49.2;




#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <eeprom.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_bt.h"
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.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




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








// --- 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
double faultyPDBRE;
long runtimeSeconds = 0;
const int crystalCorrection = 0;  // Crystal correction factor
int adjustmentsMade;
volatile double currentTime;  //changed from long
float xtalFreq;
float totalXtalFreq;
float avgXtalFreq;
int fault = 0;
int firstFault;




// --- Circular Buffer Variables ---
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;




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




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












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








    //wait for variables to stabilise;
    if (runtimeSeconds == 0) {
      globalPulseCount = 9;
      faultyPDBRE = 0;
      totalXtalFreq = 90;
      fault = 0;
    }




    // Process the pulse




    if (pulseDurationBetweenRisingEdges >= 99980 && pulseDurationBetweenRisingEdges <= 100020) {
      pulseCount++;
    } else {
      fault++;
      if (fault == 1) {
        firstFault = runtimeSeconds;
        faultyPDBRE = pulseDurationBetweenRisingEdges;
      }
    }




    xtalFreq = ((10000000 * (100000 / pulseDurationBetweenRisingEdges))) / 1000000;
    if (runtimeSeconds > 0) {
      totalXtalFreq += xtalFreq;
      avgXtalFreq = totalXtalFreq / globalPulseCount;
    }




    displayVariables();
    globalPulseCount++;




    //1 minute tasks
    if (runtimeSeconds % 60 == 0) {
      freeMemory = ESP.getFreeHeap();
      nistUpdate();
      differenceTime = (unix - nistTime);
    }




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




    if (pulseCount >= 10) {
      if (runtimeSeconds > 0) {
        totalPDBRE = totalPDBRE + pulseDurationBetweenRisingEdges;  // Update total pulse duration
        //calculate a longer term average pulse duration
        averagePDBRE = totalPDBRE / (runtimeSeconds + 1);
      }
      runtimeSeconds++;
      pulseCount = 0;
      microSeconds += (microChange * 10);  //add 10 x microchange as ten pulses per second




      //Adjust display so microSeconds reamining is shown in hours between pulse adjustments
      if (timeAdjust != 0) {
        if (microSeconds < 0) {
          // displayMicroSeconds = 100000 + microSeconds;
          displayMicroSeconds = round((100000 + microSeconds) / (10 * abs(microChange) * 3600) * 1000) / 1000;




        } else if (microSeconds > 0) {
          //displayMicroSeconds =100000 - microSeconds;
          displayMicroSeconds = round((100000 - microSeconds) / (10 * microChange * 3600) * 1000) / 1000;
        }
      }
      // Update time, display once per succesful 10 pulse cycle




      updateTime();
      updateDisplay();
    }
  }
}




/// --- 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 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 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++;
  nistTime++;
  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);
  unix = preferences.getLong("unix", 0);
  visitorsCount = preferences.getInt("visitorsCount", 0);




  preferences.end();
}




void saveToEEPROM() {
  preferences.begin("clock", false);  // Open a namespace (false = read/write mode)




  preferences.putInt("microChange", microChange);
  preferences.putFloat("timeAdjust", timeAdjust);
  preferences.putLong("unix", unix);
  preferences.putInt("visitorsCount", visitorsCount);




  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(" TimeStored: ");
  Serial.print(currentTime);
  Serial.print(" LRET: ");
  Serial.print(lastRisingEdgeTime);
  Serial.print(" BH: ");
  Serial.print(bufferHead);
  Serial.print(" BT: ");
  Serial.print(bufferTail);
  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());




  // Set up route handlers
  routeHandlers();




  // Start the server
  server.begin();




  // 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["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["firstFault"] = firstFault;
  doc["globalPulseCount"] = globalPulseCount;
  doc["faultyPDBRE"] = faultyPDBRE;
  doc["totalPDBRE"] = totalPDBRE;




  // 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;
  nistTime = (mktime(&timeinfo));
  microSeconds = 0;  // Reset microsecond adjustments
  pulseCount = 0;
  stamp.getDateTime(unix);




  // Debug Output
  Serial.println("NTP Sync Successful!");
  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 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) {
    fault = 0;
    firstFault = 0;
    firstFault = 0;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Fault Reset");
    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: 600px;
    margin: 20px auto;
}




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




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




.header {
    font-size: 2.5rem;
    font-weight: bold;
    color: #00ff00;
    text-align: center;
}




.header2 {
    font-size: 1.5rem;
    font-weight: bold;
    color: #ffdd00;
    text-align: center;
    margin-top: -10px;
}




.subheader {
    font-size: 1.2rem;
    color: #bbb;
    text-align: center;
    margin-top: 10px;
    font-style: italic;
}




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




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




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




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




/* General button style */
button {
    font-family: 'Arial', sans-serif;
    font-size: 1.2rem;
    padding: 10px 20px;
    border: none;
    border-radius: 8px;
    background-color: #4CAF50; /* Green background */
    color: white;
    cursor: pointer;
    transition: background-color 0.3s ease, transform 0.2s ease; /* Smooth transition */
}




/* Hover effect */
button:hover {
    background-color: #45a049; /* Darker green */
    transform: scale(1.05); /* Slightly enlarges on hover */
}




/* Active button (when clicked) */
button:active {
    background-color: #3e8e41; /* Even darker green */
    transform: scale(0.98); /* Slightly shrinks on click */
}




/* Disabled button */
button:disabled {
    background-color: #b0b0b0; /* Gray background */
    color: #d3d3d3; /* Light gray text */
    cursor: not-allowed;
    transform: none; /* No scaling on disabled buttons */
}




/* Focus outline */
button:focus {
    outline: none; /* Remove default focus outline */
    box-shadow: 0 0 5px rgba(72, 133, 255, 0.5); /* Add custom focus outline */
}




/* Optional: Add some padding to buttons in a container */
.button-container {
    display: flex;
    gap: 20px;
    justify-content: center;
    margin-top: 20px;
}
        @media (max-width: 1024px) {
            .time { font-size: 6rem; }
            .variable { font-size: 2rem; }
            .header, .header2 { font-size: 3rem; }
            .footer { font-size: 1.5rem; }
            .description { font-size: 1rem; }
            
        }
        @media (max-width: 768px) {
            .time { font-size: 8rem; }
            .variable { font-size: 2.5rem; }
            .header, .header2 { font-size: 3rem; }
            .footer { font-size: 2rem; }
            .description { font-size: 1rem; }
        }




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




  .button-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Adjust button width */
    gap: 10px; /* Space between buttons */
    justify-items: center;
    margin-top: 20px;
  }




  .button-grid button {
    padding: 10px 20px;
    font-size: 16px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: background-color 0.3s;
  }




  .button-grid button:hover {
    background-color: #45a049;
  }     
    </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('firstFault').innerText = data.firstFault;
                    document.getElementById('globalPulseCount').innerText = data.globalPulseCount;
                    document.getElementById('faultyPDBRE').innerText = data.faultyPDBRE;
                });
        }
        setInterval(updateVariables, 250);
    </script>


    <div class="container">
    <div class="header">ElectroKev Atomic Clock Project
    <span class="header2">(EACP)</span></div>
    <div class="subheader">Developed by Kevin Davy | 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>




    <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: orangered;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
            <div class="variable" style="color: turquoise;"><strong>EACP UNIX Time:<br></strong> <span id="unixTime">-</span></div>
            <div class="variable" style="color: yellow;"><strong>NIST UNIX Time:<br></strong> <span id="nistTime">-</span></div>
            <div class="variable"><strong>Accuracy within:<br></strong> <span id="differenceTime">-</span> seconds</div>
            <div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
            <div class="variable"><strong>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
            <div class="variable"><strong>μs/Pulse+-:<br></strong> <span id="microChange">-</span></div>
            <div class="variable"><strong>PulseAdj/Next:<br></strong>  <span id="adjustmentsMade">-</span> / <span id="displayMicroSeconds">-</span> Hrs</div>
            <div class="variable"><strong>PDBRE:<br></strong> <span id="pulseDurationBetweenRisingEdges">-</span> μS</div>
            <div class="variable"><strong>Avg PDBRE:<br></strong> <span id="averagePDBRE">-</span> μs</div>
            <div class="variable"><strong>Adjust:<br></strong> <span id="timeAdjust">-</span> s/Day</div>
            <div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> KB</div>
            <div class="variable"><strong>RunTime (1hr)<br></strong> <span id="runtimeSeconds">-</span> / 3600</div>
            <div class="variable"><strong>Total PDBRE<br></strong> <span id="totalPDBRE">-</span> μs</div>
            <div class="variable"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
            <div class="variable"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
            <div class="variable" style="color: red;"><strong>Invalid Pulses:<br></strong> <span id="fault">-</span></div>
            <div class="variable" style="color: red;"><strong>First Fault at:<br></strong> <span id="firstFault">-</span> Sec <span id="faultyPDBRE">-</span></div>
            
        </div>
    
      
<div class="description">
<div class="admin-controls">
  <p>Administrator Controls - Auth Use Only</p>
  
  <!-- Unlock Section -->
  <div id="unlock-section">
    <input type="password" id="admin-password" placeholder="Enter Password">
    <button onclick="unlockControls()">Unlock Controls</button>
  </div>
  
  <!-- Buttons Section (Initially hidden) -->
  <div id="button-section" style="display: none;">
    <div class="button-grid">
      <button onclick="syncNTP()">Sync with NTP</button>
      <button onclick="microUP()">microUP</button>
      <button onclick="microDOWN()">microDOWN</button>
      <button onclick="timeAdjustINC()">mSADJDAYINC</button>
      <button onclick="timeAdjustDEC()">mSADJDayDEC</button>
      <button onclick="lockEEPROM()">EEPROM</button>
      <button onclick="bufferReset()">Fault Reset</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)">
        NEVER Press this Button
      </button>
    </div>
  </div>
</div>




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




<script>
  // password for control panel
  const correctPassword = "kev"; 




  function unlockControls() {
    const enteredPassword = document.getElementById("admin-password").value;
    
    if (enteredPassword === correctPassword) {
      document.getElementById("button-section").style.display = "block";  // Show the buttons
      document.getElementById("unlock-section").style.display = "none";  // Hide the password input
    } else {
      alert("Password Incorrect");
    }
  }
</script>




</div>
<div class="description">
  <h3>ElectroKev Atomic Clock Features</h3>
  <div class="mainlinks">
  <a href="https://electrokev.com/eacp-code" target="_blank">** Source Code ** View Source Code v47.6</a><br>
  <a href="https://electrokev.com/eacp-code-v48-3/" target="_blank">** Source Code ** View Source Code v48.3</a><br>
  <a href="https://electrokev.com/eacp-code-v48-6/" target="_blank">** Source Code ** View Source Code v48.6</a><br>
  <a href="https://electrokev.com/eacp-code-v49-0/" target="_blank">** Source Code ** View Source Code v49.0</a><br>
  </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>
 
    </div>
    
   <div class="footer">
    <a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
    © Kevin Davy 2025
    <br><br> Last Code Upload: )rawliteral"
                + String(buildDate) + " at " + String(buildTime) + R"rawliteral(
</div>

<p></p>

)rawliteral";




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