/****************************************************************************
* ElectroKev Atomic Clock
* Author: Kevin Davy ([email protected])
* © 2025 - All Rights Reserved
*
* Written in C++, HTML, Javascript and CSS
* Hardware: ESP32 Dual Core Microcontroller
* Crystal: Oven controlled 10MHz Square/Sine Mode
*
* Description:
* - Pulse-modulated clock synchronized to an external 10 MHz pulse.
* - Achieves fine pulse manipulation with accuracy up to 1 microsecond
* per 24 hours for precise timekeeping.
* - Segregated NIST time reference for synchronization.
* - Manual controls for time and date with pulse injection or removal.
* - Rotary encoder used to adjust daily time alteration rate,
* with automatic calculation for microseconds per pulse.
* - WiFi connectivity enabled for remote monitoring and control.
* - Continuous crystal speed monitoring to debug drift.
* - Web interface providing full control over all major clock functions
* via server handlers.
* - Advanced fault detection for invalid pulse readings,
* with automatic pulse removal and logging of debug data.
* - Regulated and finely tuned power design with individual power supplies
* for stable performance.
* - OTA updates for seamless firmware updates over the air.
* - Dual-core configuration with timekeeping processes separate from other
* resource-intensive functions for enhanced performance.
* - Pulse monitoring inputs configured on high-priority processor interrupts
* for accurate counting.
* - Globally accessible NTP time server running on port 123,
* providing highly accurate time synchronization.
* - Web based serial console with self monitoring error reporting.
* providing real time debugging and self correction.
*
***************************************************************************/
/* RUN EVENTS ON CORE 0
RUN WEBSERVER ON CORE 1
RUN ESP32 ON CORE 0
RUN PULSEPROCESSBUFFER ON CORE 1
RUN NTP REQUEST SERVER ON 1 */
const float versionNumber = 54.1;
#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.h>
#include <wifiudp.h>
WiFiUDP ntpUDP;
/* ──────────────────────────────────────────────────────────────── */
/* 🛰️ NETWORK & NTP CONFIGURATION */
/* ──────────────────────────────────────────────────────────────── */
// Wi-Fi credentials
const char *ssid = SSID; // Wi-Fi network name
const char *password = PASSWORD; // Wi-Fi password
char ntpClientIP[16]; // Stores NTP client IP address
// Static IP configuration
IPAddress local_ip(192, 168, 1, 172); // Local static IP address
IPAddress gateway(192, 168, 1, 1); // Router gateway IP
IPAddress subnet(255, 255, 255, 0); // Subnet mask
// Web server setup
AsyncWebServer server(8080); // Web server on port 8080
Preferences preferences; // Persistent storage for settings
// NTP Configuration
const char *ntpServer = "time.google.com"; // Default NTP server
//const char *ntpServer = "ntp.my-inbox.co.uk";
const int NTP_PORT = 123; // NTP protocol port
const long gmtOffset_sec = 1; // UTC offset (no time zone shift)
const int daylightOffset_sec = 0; // Daylight saving time offset
long ntpRequests; // Counter for NTP requests received
const int NTP_PACKET_SIZE = 48;
byte ntpPacketBuffer[NTP_PACKET_SIZE];
/* ──────────────────────────────────────────────────────────────── */
/* 🏗️ BUILD INFORMATION */
/* ──────────────────────────────────────────────────────────────── */
const char *buildDate = __DATE__; // Compilation date
const char *buildTime = __TIME__; // Compilation time
/* ──────────────────────────────────────────────────────────────── */
/* 📌 PIN CONFIGURATION */
/* ──────────────────────────────────────────────────────────────── */
// Rotary Encoder
#define ROTARY_CLK 26 // Clock signal for rotary encoder
#define ROTARY_DT 27 // Data signal for rotary encoder
#define ROTARY_SW 13 // Switch button on rotary encoder
// Time Adjustment Buttons
#define adjustHoursPin 16 // Button to adjust hours
#define adjustMinutesPin 17 // Button to adjust minutes
#define adjustSecondsPin 5 // Button to adjust seconds
#define adjustMicroSecondUp 18 // Button to increase microseconds
#define adjustMicroSecondDown 19 // Button to decrease microseconds
// Other Pins
#define syncNTC 23 // Synchronization input pin
#define pulsePin 32 // Pulse input pin for clock synchronization
#define BUFFER_SIZE 50 // Circular buffer size for pulse tracking
/* ──────────────────────────────────────────────────────────────── */
/* 🌍 GLOBAL OBJECTS */
/* ──────────────────────────────────────────────────────────────── */
UnixTime stamp(0); // Handles UNIX timestamp calculations
UnixTime nistStamp(0); //Handles NistTime timestamp calculations
LiquidCrystal_I2C lcd(0x27, 20, 4); // LCD display (I2C, 20x4 characters)
/* ──────────────────────────────────────────────────────────────── */
/* ⏳ CLOCK & TIMEKEEPING VARIABLES */
/* ──────────────────────────────────────────────────────────────── */
double microSeconds; // Microsecond adjustments
double displayMicroSeconds; // Displayed microsecond value
double microChange; // Microsecond change factor
float rotaryChangeValue = 0.1; // Step size for rotary encoder adjustments
float timeAdjust; // Time correction factor
double pulseDurationBetweenRisingEdges; // Time between clock pulses
double invalidPulse; // Stores invalid pulse duration
double averagePDBRE; // Average pulse duration
double totalPDBRE; // Sum of pulse durations
double totalPDBREAC = 100000; // Accumulator for long-term precision
long long espRuntime; // ESP32 uptime in seconds
long runtimeSeconds; // System runtime in seconds
long runtimeOneMinute; // Runtime in minutes
long runtimeHours; // Runtime in hours
const int crystalCorrection = 0; // Correction factor for crystal oscillator
int adjustmentsMade; // Total adjustments performed
double xtalFreq; // Measured crystal frequency
double totalXtalFreq; // Accumulator for frequency calculations
double avgXtalFreq; // Average calculated crystal frequency
int lastUpdate; // Last update timestamp
String timeNow; // Current formatted time as string
unsigned long lastMillis; // Last recorded millisecond timestamp
double nistMicros;
double nistAdjuster=0;
double nistCalibrate=0;
double compensateVariable=0;
double oneWayDelay;
double preciseUnix;
unsigned long unixMs;
/* ──────────────────────────────────────────────────────────────── */
/* ⚠️ FAULT HANDLING VARIABLES */
/* ──────────────────────────────────────────────────────────────── */
int fault; // Fault counter
int faultTime; // Timestamp of last fault
bool generalFault = true; // Flag for general faults
String status = "FAULT"; // System status
String faultCode = "Fault: Unexpected Restart"; // Boot Up default message prior to calibration
String flagOtherMessage = " "; // Stores additional fault messages
double faultyPDBRE; // Stores pulse duration of a fault
bool flagInvalidPulse; // Flag for invalid pulses
bool flagTimeDrift; // Flag for time drift detection
bool flagUnexpectedRestart = true; // Flag for unexpected ESP32 resets
bool flagNTPReceived; // Flag for received NTP requests
bool flagNTPSync; // Flag for NTP synchronization success
bool flagVisitor; // Flag for visitor activity
bool flagStackSize; // Flag for stack size issues
bool flagOther; // Generic flag for additional faults
const int MAX_FAULT_LENGTH = 7000; // Max characters stored in fault log
bool clockLock=false;
/* ──────────────────────────────────────────────────────────────── */
/* 🔄 CIRCULAR BUFFER FOR PULSE TRACKING */
/* ──────────────────────────────────────────────────────────────── */
volatile double currentTime; // Timestamp of latest pulse
volatile double nextHead; // Next head position in buffer
volatile double pulseTimes[BUFFER_SIZE]; // Buffer storing pulse durations
volatile uint32_t bufferHead = 0; // Head index of circular buffer
volatile uint32_t bufferTail = 0; // Tail index of circular buffer
double lastRisingEdgeTime; // Last recorded rising edge timestamp
int pulseCount; // Number of pulses detected
double globalPulseCount; // Global pulse count since boot
/* ──────────────────────────────────────────────────────────────── */
/* 📦 GENERAL VARIABLES */
/* ──────────────────────────────────────────────────────────────── */
bool updateDisplayFlag=true;
bool dateSetting = 0; // Flag for date-setting mode
double nistTime; // Stores latest NIST time update
double unix; // Current UNIX timestamp
double differenceTime; // Difference between NTP and local time
double lastNistUpdate; // Timestamp of last NIST update
double bootupTime; // System boot timestamp
int visitorsCount; // Total visitors to the web interface
int freeMemory = 0; // Available heap memory
String clientIP; // Stores latest client IP address
double stackSize; // Stack size monitoring variable
bool NTPON=true;
// --- Interrupt Service Routine (ISR) ---
IRAM_ATTR void pulseISR() {
currentTime=esp_timer_get_time();
nextHead = (bufferHead + 1) % BUFFER_SIZE;
if (nextHead != bufferTail) { // Check if the buffer is not full
pulseTimes[bufferHead] = currentTime;
bufferHead = nextHead;
}
}
// --- Setup Function ---
void setup() {
Serial.begin(115200);
// --- WiFi and OTA Setup ---
setupWifi(); // Starts WiFi and web server and NTP server (runs on Core 0)
Serial.println("Starting OTA...");
ArduinoOTA.begin();
Serial.println("OTA Updates Ready");
ArduinoOTA.setPassword(nullptr); // Disable password
// --- Rotary Encoder Pins Setup ---
pinMode(ROTARY_CLK, INPUT);
pinMode(ROTARY_DT, INPUT);
pinMode(ROTARY_SW, INPUT_PULLUP);
// --- Pulse Pin Setup ---
pinMode(pulsePin, INPUT);
attachInterrupt(digitalPinToInterrupt(pulsePin), pulseISR, FALLING);
// --- LCD Initialization ---
lcd.init();
lcd.begin(20, 4);
lcd.backlight();
// --- Button Pins Setup ---
pinMode(adjustHoursPin, INPUT_PULLUP);
pinMode(adjustMinutesPin, INPUT_PULLUP);
pinMode(adjustSecondsPin, INPUT_PULLUP);
pinMode(adjustMicroSecondDown, INPUT_PULLUP);
pinMode(adjustMicroSecondUp, INPUT_PULLUP);
pinMode(syncNTC, INPUT_PULLUP);
// --- Load Stored Data ---
loadFromEEPROM();
// --- Task: Process NTP Requests --- //start this back up
xTaskCreatePinnedToCore(
taskProcessNTPRequests, // Task function
"ProcessNTPRequests", // Task name
8096, // Stack size
NULL, // Task parameters
4, // Priority (higher than pulse processing)
NULL, // Task handle
1 // Core
);
// --- Task: Main Buffer Processor
xTaskCreatePinnedToCore(
taskProcessPulseBuffer, // Task function
"ProcessPulses", // Task name
4096, // Stack size
NULL, // Task parameters
4, // Priority (higher than pulse processing)
NULL, // Task handle
1 // Core
);
xTaskCreatePinnedToCore(
ntpRestartTask, // Task function
"NTP_Restart", // Task name
4048, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core
);
xTaskCreatePinnedToCore(
taskUpdateDisplay, // Task function
"DisplayUpdate", // Task name
8048, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core
);
xTaskCreatePinnedToCore(
taskOneHourSelfCheck, // Task function
"SelfCheck", // Task name
4096, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core
);
// --- Startup Confirmation ---
Serial.println("Running");
flagUnexpectedRestart = true;
faultHandler();
// --- Sync Time from NTP ---
syncWithNTP();
// stamp.getDateTime(unix);
}
// --- Main Loop ---
void loop() {
handleButtons(); // Check and process button inputs
handleRotaryEncoder(); // Handle rotary encoder inputs
adjustmentRoutine(); // Adjust time if microseconds overflow
ArduinoOTA.handle();
vTaskDelay(100);
}
void taskProcessPulseBuffer(void *pvParameters) {
while (true) {
processPulseBuffer();
vTaskDelay(pdMS_TO_TICKS(100)); // limit function execution frequency
}
}
void taskUpdateDisplay(void *pvParameters) {
// Run indefinitely with a 0.1-second delay
while (true) {
if (updateDisplayFlag){
updateDisplay(); // Call the function to update the internal screen.
updateDisplayFlag=false;
}
vTaskDelay(pdMS_TO_TICKS(20)); // limit function execution frequency
}
}
void taskOneHourSelfCheck(void *pvParameters) {
while (true) {
vTaskDelay(pdMS_TO_TICKS(3600000));
saveToEEPROM();
String allOK="| Automatic Self Check: "+status+ " | Drift: "+differenceTime;
totalPDBREAC = 0;
totalPDBRE = 0;
runtimeHours++;
averagePDBRE = 0;
totalPDBREAC=100000;
avgXtalFreq = 0;
totalXtalFreq = 0;
globalPulseCount = 0;
runtimeSeconds = 0;
customMessage(allOK);
}
}
void taskProcessNTPRequests(void *pvParameters) {
while (true) {
processNTPRequests(); // Call the function to process NTP requests
//checkStack();
vTaskDelay(pdMS_TO_TICKS(50)); // limit function execution frequency
}
}
void ntpRestartTask(void *pvParameters) {
while (true) {
vTaskDelay(1800000 / portTICK_PERIOD_MS); // Wait for 1 hour
ntpUDP.stop(); // Stop current UDP server
vTaskDelay(100 / portTICK_PERIOD_MS); // Short delay
ntpUDP.begin(NTP_PORT); // Restart NTP server
customMessage("NTP Check: OK");
}
}
void faultCheck(){
generalFault = flagUnexpectedRestart || flagTimeDrift; // Set generalFault
status = generalFault ? "FAULT" : "OK "; // Update status
}
void faultHandler() {
// If faultCode exceeds max length, trim the oldest part
if (faultCode.length() > MAX_FAULT_LENGTH) {
faultCode = faultCode.substring(0, MAX_FAULT_LENGTH);
}
// Handle unexpected restart
if (flagUnexpectedRestart) {
esp_reset_reason_t resetReason = esp_reset_reason();
String resetReasonText;
switch (resetReason) {
case ESP_RST_POWERON: resetReasonText = "POWERON"; break;
case ESP_RST_BROWNOUT: resetReasonText = "BROWNOUT"; break;
case ESP_RST_SW: resetReasonText = "SW"; break;
case ESP_RST_EXT: resetReasonText = "EXT"; break;
case ESP_RST_PANIC: resetReasonText = "MEMPANIC"; break;
case ESP_RST_WDT: resetReasonText = "WATCHDOG"; break;
default: resetReasonText = "UNKNOWN"; break;
}
faultCode = timeNow + " Fault: Unexpected Restart ESP_RST_" + resetReasonText +
" " + String(espRuntime) + " Sec\n" + faultCode;
generalFault = true;
flagUnexpectedRestart = false;
}
// Handle specific faults
if (flagTimeDrift) {
faultCode = timeNow + " Fault: Time Drift -> " + String(differenceTime) + "\n" + faultCode;
generalFault = true;
flagTimeDrift = false;
}
if (flagInvalidPulse) {
faultCode = timeNow + " Warning: Invalid Pulse Detected (" + String(invalidPulse) + ")\n" + faultCode;
flagInvalidPulse = false;
}
// Log events (without triggering generalFault)
if (flagNTPReceived) {
faultCode = timeNow + " NTP Request Received from [IP: " + ntpClientIP + "]\n" + faultCode;
ntpRequests++;
flagNTPReceived = false;
}
if (flagNTPSync) {
faultCode = timeNow + " NTP Successfully Synced\n" + faultCode;
flagNTPSync = false;
}
if (flagVisitor) {
faultCode = timeNow + " Web User Logged In [IP:" + clientIP + "]\n" + faultCode;
flagVisitor = false;
}
if (flagStackSize) {
faultCode = timeNow + " WARNING: StackSize Remaining: " + String(stackSize) + " bytes\n" + faultCode;
flagStackSize = false;
}
if (flagOther) {
faultCode = timeNow + " Message: " + flagOtherMessage + "\n" + faultCode;
flagOtherMessage.clear();
flagOther = false;
}
// Save fault status and update system status
status = generalFault ? "FAULT" : "OK ";
saveToEEPROM();
}
void customMessage(String cm){
flagOther=true;
flagOtherMessage=cm;
faultHandler();
}
void processPulseBuffer() {
if (!clockLock){
while (bufferTail != bufferHead) {
// Disable interrupts temporarily to process the pulse
noInterrupts();
double pulseTime = pulseTimes[bufferTail];
bufferTail = (bufferTail + 1) % BUFFER_SIZE;
pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime) + crystalCorrection;
lastRisingEdgeTime = pulseTime;
interrupts(); // Re-enable interrupts
// Initialize variables at startup
if (runtimeSeconds == 0) {
globalPulseCount = 9;
totalXtalFreq = 90;
fault = 0;
}
// Validate the pulse duration
if (pulseDurationBetweenRisingEdges >= 60000 && pulseDurationBetweenRisingEdges <= 140000) {
pulseCount++;
unix=unix+0.100;
nistTime=nistTime+0.100;
// Calculate crystal frequency
xtalFreq = 1000000 / pulseDurationBetweenRisingEdges;
} else {
invalidPulse = pulseDurationBetweenRisingEdges;
fault++;
flagInvalidPulse = true;
faultHandler();
}
// Update crystal frequency tracking
if (runtimeSeconds > 0) {
totalXtalFreq += xtalFreq;
avgXtalFreq = totalXtalFreq / globalPulseCount;
}
globalPulseCount++;
// Perform specific high frequency tasks
if (runtimeOneMinute >= 60) {
//checkDifference();
nistUpdate();
lastNistUpdate = nistTime;
runtimeOneMinute = 0;
}
// Reset after 1 hour
if (runtimeSeconds >= 3600) {
}
// Process accumulated pulses (every 10 pulses)
if (pulseCount >= 10) {
// Update time and display once per successful 10-pulse cycle
updateTime();
checkDifference();
reportVariables();
if (runtimeSeconds > 0) {
totalPDBREAC += pulseDurationBetweenRisingEdges;
totalPDBRE = ((runtimeSeconds + 1) * 100000) - totalPDBREAC;
averagePDBRE = totalPDBREAC / (runtimeSeconds + 1);
}
// Track system runtime
runtimeSeconds++;
pulseCount = 0;
espRuntime++;
runtimeOneMinute++;
microSeconds += (microChange * 10);
freeMemory = ESP.getFreeHeap();
checkStack();
timeAdjustCorrections();
}
}
} // closes clockLock
}
void timeAdjustCorrections(){
// Adjust display for remaining microseconds per hour
if (timeAdjust != 0) {
if (microSeconds < 0) {
displayMicroSeconds = round((100000 + microSeconds) / (10 * abs(microChange) * 3600) * 1000) / 1000;
} else if (microSeconds > 0) {
displayMicroSeconds = round((100000 - microSeconds) / (10 * microChange * 3600) * 1000) / 1000;
}
// Update microseconds adjustment
microChange = ((timeAdjust * 1000000) / 864000);
// Correct very small timeAdjust values close to 0
if (abs(timeAdjust) < 0.1) {
timeAdjust = 0.0;
microSeconds = 0;
microChange = 0;
displayMicroSeconds = 0;
}
}
}
void checkDifference() {
differenceTime = (unix - nistTime);
if (abs(differenceTime) >= 5) {
flagTimeDrift = true;
faultHandler();
customMessage("Attempting Self Repair - Fault Logged");
nistUpdate();
return;
}
}
void reportVariables(){
Serial.print(" Difference Time ");
Serial.print(differenceTime,6);
Serial.print(" Runtime Seconds ");
Serial.print(runtimeSeconds);
Serial.print(" UNIX Time ");
Serial.print(unix,6);
Serial.print(" NIST Time ");
Serial.print(nistTime,6);
Serial.print(" Compensation ");
Serial.println(compensateVariable);
}
void processNTPRequests() {
if (!NTPON){
int packetSize = ntpUDP.parsePacket();
if (packetSize == 0) return; // No new packet
if (packetSize < 48) {
customMessage("NTP request rejected. Packet too small: Flushed");
ntpUDP.flush();
return; // Ignore invalid packets
}
if (packetSize > 48) {
customMessage("NTP request rejected. Packet too large: Flushed");
ntpUDP.flush();
return; // Ignore invalid packets
}
Serial.print("NTP request received: ");
byte packetBuffer[48]; // Create response buffer
memset(packetBuffer, 0, 48);
ntpUDP.read(packetBuffer, packetSize); // Read the request
// Ensure valid NTP request (mode 3 - client mode)
byte mode = packetBuffer[0] & 0b00000011; // Extract the last 3 bits (mode)
if (mode != 3) { // Check if it's a client request (mode 3)
customMessage("NTP request rejected. Packet Mode Invalid: Flushed");
ntpUDP.flush();
return;
}
// Copy client's transmit timestamp (bytes 40-47) into origin timestamp (bytes 24-31)
memcpy(&packetBuffer[24], &packetBuffer[40], 8);
// Set response header: LI = 0 (No Warning), Version = 4, Mode = 4 (Server)
packetBuffer[0] = 0b00100100;
// Stratum 2 (Secondary time source)
packetBuffer[1] = 2;
// Polling interval
packetBuffer[2] = 6;
// Precision declaration
packetBuffer[3] = -20;
// Root Delay (Fixed-point format, 1ms)
packetBuffer[4] = 0x00;
packetBuffer[5] = 0x01;
packetBuffer[6] = 0x00;
packetBuffer[7] = 0x00;
// Root Dispersion (Fixed-point, 1)
packetBuffer[8] = 0x00;
packetBuffer[9] = 0x01;
packetBuffer[10] = 0x00;
packetBuffer[11] = 0x00;
// Reference Identifier
packetBuffer[12] = 'G';
packetBuffer[13] = 'P';
packetBuffer[14] = 'S';
packetBuffer[15] = 0;
// Get the current epoch time
unsigned long currentEpoch = unix;
unsigned long ntpSeconds = currentEpoch + 2208988800UL;
// Convert pulseCount (1/10s increments) into NTP fractional format
unsigned long ntpFractionSend = (pulseCount * 429496729) % 4294967296UL;
// Reference Timestamp (last time this clock was updated)
packetBuffer[16] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[17] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[18] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[19] = ntpSeconds & 0xFF;
packetBuffer[20] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[21] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[22] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[23] = ntpFractionSend & 0xFF;
// Receive Timestamp (time request was received)
packetBuffer[32] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[33] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[34] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[35] = ntpSeconds & 0xFF;
packetBuffer[36] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[37] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[38] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[39] = ntpFractionSend & 0xFF;
// Transmit Timestamp (time response is sent)
packetBuffer[40] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[41] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[42] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[43] = ntpSeconds & 0xFF;
packetBuffer[44] = (ntpFractionSend >> 24) & 0xFF;
packetBuffer[45] = (ntpFractionSend >> 16) & 0xFF;
packetBuffer[46] = (ntpFractionSend >> 8) & 0xFF;
packetBuffer[47] = ntpFractionSend & 0xFF;
// Debug Output
Serial.print("NTP Response Sent: ");
Serial.print(ntpSeconds);
Serial.print(".");
Serial.print(ntpFractionSend);
Serial.print(" IP:");
// Send the response
ntpUDP.beginPacket(ntpUDP.remoteIP(), ntpUDP.remotePort());
ntpUDP.write(packetBuffer, 48);
ntpUDP.endPacket();
// Retrieve and print the IP address of the requesting client
Serial.println(ntpUDP.remoteIP());
// get client IP for NTP
ntpUDP.remoteIP().toString().toCharArray(ntpClientIP, sizeof(ntpClientIP));
flagNTPReceived = true;
faultHandler();
}
}
void checkStack(){
// Check stack size
stackSize = uxTaskGetStackHighWaterMark(NULL);
if (stackSize < 1400) {
flagStackSize = true;
faultHandler();
}
}
/// --- Handle Rotary Encoder Inputs ---
void handleRotaryEncoder() {
static int lastEncoderState = HIGH;
int currentEncoderState = digitalRead(ROTARY_CLK);
static unsigned long lastEncoderChangeTime = 0;
const unsigned long debounceDelay = 80; // Increased debounce delay
// Check for debounce timing
if ((millis() - lastEncoderChangeTime) > debounceDelay) {
if (currentEncoderState != lastEncoderState) {
// If the direction is different, increment or decrement the value by the desired rotaryChangeValue
if (digitalRead(ROTARY_DT) != currentEncoderState) {
timeAdjust += 0.1; // Adjust by 0.1
} else {
timeAdjust -= 0.1; // Adjust by -0.1
}
// Update the last change time
lastEncoderChangeTime = millis();
}
}
// Update the last encoder state
lastEncoderState = currentEncoderState;
}
// --- Handle Button Inputs ---
void handleButtons() {
static unsigned long lastButtonPressTime = 0;
const unsigned long buttonDebounceInterval = 150;
// Check for debounce interval
if (millis() - lastButtonPressTime > buttonDebounceInterval) {
// Toggle dateSetting if rotary switch is pressed
if (digitalRead(ROTARY_SW) == LOW) {
saveToEEPROM();
dateSetting = !dateSetting; // Toggle the value of dateSetting
}
// Adjust time or date based on dateSetting state
if (!dateSetting) {
// Time adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 3600; // Add one hour
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 60; // Add one minute
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix -= stamp.second; // Reset seconds
pulseCount = 0;
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
pulseCount--; // Decrease pulse count
} else if (digitalRead(adjustMicroSecondUp) == LOW) {
pulseCount++; // Increase pulse count
} else if (digitalRead(syncNTC) == LOW) {
lcd.clear();
lcd.print("WiFi Toggle Button Pressed");
if (WiFi.status() == WL_CONNECTED) {
lcd.print("Turning WiFi OFF...");
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
lcd.print("Turning WiFi ON...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
}
vTaskDelay(pdMS_TO_TICKS(500));
lcd.clear();
}
} else if (dateSetting) {
// Date adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 86400; // Add one day
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 2592000; // Add one month (needs refinement for proper month handling)
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix += 31622400; // Add one year
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
unix = 1736521548;
}
}
// Update date and time
stamp.getDateTime(unix);
// Update debounce timer
lastButtonPressTime = millis();
}
}
// --- Update Time ---
void updateTime() {
//unix++;
//nistTime++;
stamp.getDateTime(unix);
// Extract the fractional part for milliseconds
unixMs = (unix - floor(unix)) * 1000;
// Format time string including milliseconds
timeNow = "[" + String(formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second) + "." + String(unixMs)) + "]";
updateDisplayFlag=true;
}
// --- Update Display ---
void updateDisplay() {
if (dateSetting) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Date Setting Mode");
lcd.setCursor(0, 1);
lcd.print("Adj:");
lcd.print(timeAdjust, 1);
lcd.print(" ");
lcd.print(displayMicroSeconds); // Display positive microseconds
return;
}
// Display full time and date
lcd.setCursor(0, 0);
displayFullTimeDate();
// Display UTC time
lcd.setCursor(0, 1);
lcd.print("UTC: ");
lcd.print(unix,3); // Display Unix timestamp
lcd.setCursor(0, 2);
lcd.print("NTP: ");
lcd.print(nistTime,3);
// Display calculated crystal frequency
lcd.setCursor(0, 3);
lcd.print(avgXtalFreq, 6);
lcd.print(" MHz ");
// Display pulse adjustments
lcd.setCursor(14, 3);
lcd.print(status);
}
// --- Adjustment Routine ---
void adjustmentRoutine() {
if (microSeconds >= 100000) {
microSeconds = 0;
pulseCount++;
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);
runtimeHours = preferences.getDouble("runtimeHours", 0);
ntpRequests = preferences.getLong("ntpRequests");
preferences.end();
}
void saveToEEPROM() {
preferences.begin("clock", false); // Open a namespace (false = read/write mode)
preferences.putInt("microChange", microChange);
preferences.putString("faultCode", faultCode);
preferences.putFloat("timeAdjust", timeAdjust);
preferences.putDouble("runtimeHours", runtimeHours);
preferences.putLong("ntpRequests",ntpRequests);
preferences.end(); // Close the Preferences storage
}
// --- Format Time ---
String formatTime(int timeValue) {
return (timeValue < 10) ? "0" + String(timeValue) : String(timeValue);
}
void displayFullTimeDate() {
// Get the current time and date from the Unix timestamp
int currentYear = (stamp.year); // Current year
int currentMonth = (stamp.month); // Current month (1-12)
int currentDay = (stamp.day); // Current day
int currentHour = (stamp.hour); // Current hour
int currentMinute = (stamp.minute); // Current minute
int currentSecond = (stamp.second); // Current second
// Display time and date on the LCD
lcd.print(currentDay);
lcd.print("/");
lcd.print(currentMonth);
lcd.print("/");
lcd.print(currentYear);
// lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(formatTime(currentHour));
lcd.print(":");
lcd.print(formatTime(currentMinute));
lcd.print(":");
lcd.print(formatTime(currentSecond));
}
// --- Display Variables in Serial Monitor ---
void displayVariables() {
Serial.print(" RT: ");
Serial.print(runtimeSeconds);
Serial.print(" PC: ");
Serial.print(pulseCount);
Serial.print(" GPC: ");
Serial.print(globalPulseCount);
Serial.print(" PDBRE: ");
Serial.print(pulseDurationBetweenRisingEdges);
Serial.print(" unix: ");
Serial.print(unix);
Serial.print(" nistTime: ");
Serial.print(nistTime);
Serial.print(" dT: ");
Serial.print(differenceTime);
Serial.print(" xTalFreq: ");
Serial.print(xtalFreq, 6);
Serial.print(" totalXF: ");
Serial.print(totalXtalFreq, 6);
Serial.print(" avgXF: ");
Serial.println(avgXtalFreq, 6);
}
void setupWifi() {
// Begin WiFi connection
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
// Display WiFi connection details
Serial.println("Connected to WiFi");
Serial.println(WiFi.localIP());
WiFi.setSleep(false);
// Set up route handlers
routeHandlers();
// Start the server
server.begin();
Serial.println("Server started");
ntpUDP.begin(NTP_PORT);
Serial.println("NTP Server started on port 123");
}
void handleVariables(AsyncWebServerRequest *request) {
// Create a JSON document (fixed size)
StaticJsonDocument<1024> doc;
// Populate the json string
doc["unixTime"] = String(unix,3);
doc["bootupTime"] = bootupTime;
doc["nistTime"] = String(nistTime,3);
doc["date"] = String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year);
doc["time"] = formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second);
doc["displayMicroSeconds"] = displayMicroSeconds;
doc["microChange"] = microChange;
doc["pulseDurationBetweenRisingEdges"] = pulseDurationBetweenRisingEdges;
doc["timeAdjust"] = timeAdjust;
doc["differenceTime"] = String(differenceTime,6);
doc["freeMemory"] = freeMemory;
doc["averagePDBRE"] = String(averagePDBRE, 2);
doc["runtimeSeconds"] = runtimeSeconds;
doc["adjustmentsMade"] = adjustmentsMade;
doc["xtalFreq"] = String(xtalFreq, 6);
doc["avgXtalFreq"] = String(avgXtalFreq, 6);
doc["fault"] = fault;
doc["globalPulseCount"] = globalPulseCount;
doc["totalPDBRE"] = totalPDBRE;
doc["espRuntime"] = espRuntime;
doc["lastNistUpdate"] =formatTime(nistStamp.hour) + ":" + formatTime(nistStamp.minute) + ":" + formatTime(nistStamp.second);
doc["runtimeHours"] = runtimeHours;
doc["faultCode"] = String(faultCode);
doc["status"] = String(status);
doc["stackSize"] = String(stackSize);
doc["ntpRequests"] = String(ntpRequests);
doc["nistAdjuster"] = String(nistAdjuster);
doc["compensateVariable"] = String(compensateVariable);
doc["oneWayDelay"] = String(oneWayDelay);
// Serialize the JSON document to a string
String json;
serializeJson(doc, json);
// Send the JSON response
request->send(200, "application/json", json);
}
void nistUpdate() {
NTPON = true;
Serial.print("NIST Update: ");
memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE); // Clear buffer
unsigned long sendTime = millis(); // Record send time
sendNTPpacket(ntpServer);
unsigned long startWait = millis();
bool received = false;
unsigned long recvTime = 0;
while (millis() - startWait < 800) {
if (ntpUDP.parsePacket()) {
recvTime = millis(); // Record receive time
received = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(10)); // Small delay to avoid CPU overload
}
if (received) {
ntpUDP.read(ntpPacketBuffer, NTP_PACKET_SIZE);
uint32_t ntpSeconds = (ntpPacketBuffer[40] << 24) | (ntpPacketBuffer[41] << 16) |
(ntpPacketBuffer[42] << 8) | ntpPacketBuffer[43];
uint32_t ntpFraction = (ntpPacketBuffer[44] << 24) | (ntpPacketBuffer[45] << 16) |
(ntpPacketBuffer[46] << 8) | ntpPacketBuffer[47];
// Convert NTP time (from 1900 epoch) to Unix time (from 1970)
uint32_t unixSeconds = ntpSeconds - 2208988800UL;
double microseconds = ((double)ntpFraction * 1000000.0) / 4294967296.0;
// Calculate RTT and one-way delay estimate
double rtt_ms = recvTime - sendTime; // Total round-trip time (milliseconds)
oneWayDelay = rtt_ms / 2000.0; // Convert to seconds (half of RTT)
// Apply compensation
nistTime = (unixSeconds + (microseconds / 1000000.0)) - oneWayDelay + compensateVariable;
nistStamp.getDateTime(nistTime);
// Print delay information
Serial.printf("Round Trip Time: %.2f ms : ", rtt_ms);
Serial.printf("Estimated One-Way Delay: %.6f s : ", oneWayDelay);
Serial.println("NIST Update Successful");
} else {
Serial.println("Failed to NIST UPDATE.");
}
NTPON = false;
}
void syncWithNTP() {
clockLock = true; // lock the clock so it doesn't run whilst being synced for the first time
NTPON = true; // lock off NTP requests whilst UDP is being used to recieve time
visitorsCount = 0; // Reset visitor count
compensateVariable = 0; // Reset tare variable for compensating following calibration
nistAdjuster = 0; // Reset fine-tuned micro up/down
Serial.println("Connecting to NTP server...");
// Clear the buffer before sending the request
memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE);
// Send an NTP request
sendNTPpacket(ntpServer);
unsigned long startWait = millis();
bool received = false;
// Wait for up to 800ms, but exit as soon as a response is received
while (millis() - startWait < 500) {
if (ntpUDP.parsePacket()) {
received = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(10)); // Small delay to avoid CPU overload
}
if (received) {
ntpUDP.read(ntpPacketBuffer, NTP_PACKET_SIZE);
// Extract the 64-bit NTP timestamp (Seconds + Fraction)
uint32_t ntpSeconds = (ntpPacketBuffer[40] << 24) | (ntpPacketBuffer[41] << 16) |
(ntpPacketBuffer[42] << 8) | ntpPacketBuffer[43];
uint32_t ntpFraction = (ntpPacketBuffer[44] << 24) | (ntpPacketBuffer[45] << 16) |
(ntpPacketBuffer[46] << 8) | ntpPacketBuffer[47];
// Convert NTP time (from 1900 epoch) to Unix time (from 1970)
uint32_t unixSeconds = ntpSeconds - 2208988800UL;
// Convert fraction to microseconds
double microseconds = (ntpFraction * 1000000.0) / 4294967296.0;
// Store Unix time with microseconds
nistTime = unixSeconds + (microseconds / 1000000.0);
// Disable interrupts briefly for updating unix
portDISABLE_INTERRUPTS();
unix = nistTime;
portENABLE_INTERRUPTS();
checkDifference(); // Call the function to compare time against reference
lastNistUpdate = nistTime;
bootupTime = unix;
Serial.print("NTP Sync Successful: ");
Serial.print(microseconds);
Serial.print(" Microseconds ");
Serial.println(nistTime, 6); // Print with 6 decimal places
pulseCount=floor(microseconds/100000); // calibrate synced fractional seconds to pulseCount so unix starts from exact point mid second
flagNTPSync = true;
faultHandler();
// Update LCD
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("NTP Sync OK!");
} else {
Serial.println("Failed to get time from NTP server.");
lcd.clear();
lcd.print("NTP Sync Failed!");
}
NTPON = false; // allow NTP requests to recommence
clockLock = false; //restart the clock
}
void sendNTPpacket(const char* address) {
memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE); // Clear packet before filling it
ntpPacketBuffer[0] = 0b11100011; // set to mode 3 for transmission
// Send the NTP packet
IPAddress ntpIP;
WiFi.hostByName(address, ntpIP);
ntpUDP.begin(123);
ntpUDP.beginPacket(ntpIP, 123);
ntpUDP.write(ntpPacketBuffer, NTP_PACKET_SIZE);
ntpUDP.endPacket();
}
void fullReset(){
Wire.end();
vTaskDelay(100);
Wire.begin();
lcd.begin(20,4);
generalFault=false;
fault = 0;
flagInvalidPulse=false;
flagUnexpectedRestart=false;
flagTimeDrift=false;
flagNTPReceived=false;
flagNTPSync=false;
flagVisitor=false;
flagStackSize=false;
flagOther=false;
faultCode="Status: OK : StackSize = "+String(stackSize)+"\n"+"NTP Server Started(Port: [123]): OK"+"\n"+"Connection Started(clock.electrokev.com): OK"+"\n"+"Server Started(Port: [80]): OK"+"\n";
totalPDBRE = 0;
totalPDBREAC=100000;
runtimeSeconds = 0;
runtimeOneMinute=0;
runtimeHours=0;
espRuntime =0;
averagePDBRE = 0;
xtalFreq = 0;
avgXtalFreq = 0;
totalXtalFreq = 0;
globalPulseCount = 0;
//compensateVariable=0;
//nistAdjuster=0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Full Reset");
saveToEEPROM();
}
void routeHandlers() {
// Define route handlers
server.on("/", HTTP_GET, handleRoot);
server.on("/variables", HTTP_GET, handleVariables);
// Synchronize time with NTP
server.on("/syncNTP", HTTP_GET, [](AsyncWebServerRequest *request) {
syncWithNTP();
request->send(200, "text/plain", "NTP Sync Completed");
});
// Increase pulse count
server.on("/microUP", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount++;
nistAdjuster+=0.1;
//unix+=0.1;
/*lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroUp");*/
request->send(200, "text/plain", "Pulse Count Increased");
});
// Decrease pulse count
server.on("/microDOWN", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount--;
nistAdjuster-=0.1;
//unix-=0.1;
/*lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroDown");*/
request->send(200, "text/plain", "Pulse Count Decreased");
});
// Increase time adjust
server.on("/timeAdjustINC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust += 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust INC");
request->send(200, "text/plain", "Time Adjust Increased");
});
// Decrease time adjust
server.on("/timeAdjustDEC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust -= 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust DEC");
request->send(200, "text/plain", "Time Adjust Decreased");
});
// Get version number
server.on("/getVersion", HTTP_GET, [](AsyncWebServerRequest *request) {
String version = String(versionNumber, 1);
request->send(200, "application/json", "{\"version\":\"" + version + "\"}");
});
// Lock data to EEPROM and update NIST
server.on("/lockEEPROM", HTTP_GET, [](AsyncWebServerRequest *request) {
saveToEEPROM();
nistUpdate();
request->send(200, "text/plain", "Saved to EEPROM");
});
// Reset buffer
server.on("/bufferReset", HTTP_GET, [](AsyncWebServerRequest *request) {
fullReset();
request->send(200, "text/plain", "Buffer Reset");
});
// Restart the ESP32
server.on("/neverPress", HTTP_GET, [](AsyncWebServerRequest *request) {
ESP.restart();
request->send(200, "text/plain", "Never Press");
});
// compensate Up
server.on("/compensateUp", HTTP_GET, [](AsyncWebServerRequest *request) {
compensateVariable+=0.1;
request->send(200, "text/plain", "compensateUp");
});
// compensate down
server.on("/compensateDown", HTTP_GET, [](AsyncWebServerRequest *request) {
compensateVariable-=0.1;
request->send(200, "text/plain", "compensateDown");
});
// auto tare
server.on("/autoTare", HTTP_GET, [](AsyncWebServerRequest *request) {
compensateVariable=differenceTime;
customMessage("Auto Tare Activated: " + String(differenceTime));
request->send(200, "text/plain", "autoTare");
});
}
void handleRoot(AsyncWebServerRequest *request) {
clientIP=request->client()->remoteIP().toString();
visitorsCount++;
flagVisitor=true;
faultHandler();
String index_html = R"rawliteral(
<meta charset="UTF-8">
<title>ElectroKev Atomic Clock EACP</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1f1f1f, #333);
color: #e0e0e0;
font-family: 'Arial', sans-serif;
}
.time {
font-size: 4rem;
font-weight: bold;
text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
margin-bottom: 20px;
color: #00ff00;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #222;
color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 800px;
margin: 20px auto;
text-align: center;
width: 100%;
}
.variables {
display: grid;
grid-template-columns: repeat(3, 2fr);
gap: 10px;
width: 100%;
max-width: 800px;
text-align: left;
padding: 5px;
}
.variable {
font-size: 1rem;
padding: 12px 20px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.version-container {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
font-size: 1.1rem;
}
.version {
color: #fff;
font-weight: bold;
}
.last-updated {
color: #00ff00;
font-weight: normal;
}
.ntp {
color: orange;
font-weight: normal;
}
.footer {
margin-top: 30px;
font-size: 1.2rem;
text-align: center;
color: #ccc;
}
.footer a,
.mainlinks a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.description {
font-size: 1.2rem;
text-align: left;
color: #e0e0e0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
margin-top: 30px;
width: 100%;
max-width: 800px;
}
.description h3 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
}
.admin-controls {
text-align: center;
margin: 20px;
}
.serial-container {
margin-top: 30px;
width: 100%;
max-width: 800px;
height: 300px;
background-color: #1e1e1e;
color: #00ff00;
border-radius: 10px;
padding: 10px;
overflow-y: scroll;
box-shadow: 0px 0px 15px rgba(0, 255, 0, 0.1);
font-size: 22px;
white-space: pre-wrap;
}
.serial-output {
font-family: 'Courier New', monospace;
font-size: 10px;
margin: 0;
padding: 0;
line-height: 1.5;
}
.serial-output div {
padding: 2px;
}
.timestamps {
color: #ffdd00;
font-weight: bold;
}
.header {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.header2 {
font-size: 16px;
font-weight: normal;
color: #ffa500;
}
.subheader {
font-size: 16px;
color: #ccc;
margin-bottom: 15px;
}
.version-container,
.ntp-container,
.bootup-container {
margin-top: 10px;
font-size: 16px;
}
.status-label {
font-weight: bold;
margin-left: 10px; /*this one for main serial output */
color: #ffffff;
}
.status {
font-weight: bold;
}
.status[data-status="OK"] {
color: #00ff00;
}
.status[data-status="FAULT"] {
color: #ff0000;
}
/* Responsive Adjustments */
@media (max-width: 600px) {
.time { font-size: 6rem; }
.variable { font-size: 14px; }
.header, .header2 { font-size: 1rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
@media (max-width: 1024px) {
.time { font-size: 6rem; }
.variable { font-size: 24px; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
button {
font-family: 'Arial', sans-serif;
font-size: 0.8rem;
padding: 12px 20px;
border: none;
border-radius: 8px;
background: rgba(0, 255, 0, 0.2); /* Transparent green effect */
color: #00ff00;
cursor: pointer;
transition: all 0.3s ease-in-out;
backdrop-filter: blur(8px); /* Glass effect */
border: 1px solid rgba(0, 255, 0, 0.4);
box-shadow: 0 4px 10px rgba(0, 255, 0, 0.2);
text-transform: uppercase;
font-weight: bold;
letter-spacing: 1px;
width: 100%; /* Ensures uniform width */
max-width: 200px; /* Prevents stretching */
}
/* Hover Effect */
button:hover {
background: rgba(0, 255, 0, 0.3);
box-shadow: 0 6px 15px rgba(0, 255, 0, 0.4);
transform: translateY(-2px);
}
/* Active Button (When Clicked) */
button:active {
background: rgba(0, 255, 0, 0.4);
box-shadow: 0 2px 5px rgba(0, 255, 0, 0.5);
transform: scale(0.98);
}
/* Disabled Button */
button:disabled {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: not-allowed;
box-shadow: none;
}
/* Focus Effect */
button:focus {
outline: none;
box-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
}
/* Button Container & Grid */
.button-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
/* Fixed Grid for Buttons */
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); /* Ensures proper wrapping */
gap: 10px;
justify-items: center;
width: 100%; /* Makes sure the grid expands correctly */
max-width: 600px; /* Prevents excessive stretching */
margin: 0 auto; /* Centers the grid */
}
/* Mobile-Friendly Fix */
@media (max-width: 800px) {
button {
font-size: 1rem;
padding: 14px;
width: 100%;
max-width: 250px;
}
.button-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); /* Adjusts for smaller screens */
}
}
</style>
<script>
function compensateUp() {
fetch('/compensateUp')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function compensateDown() {
fetch('/compensateDown')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function autoTare() {
fetch('/autoTare')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function syncNTP() {
fetch('/syncNTP')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function microDOWN() {
fetch('/microDOWN')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function microUP() {
fetch('/microUP')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustINC() {
fetch('/timeAdjustINC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustDEC() {
fetch('/timeAdjustDEC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function bufferReset() {
fetch('/bufferReset')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function neverPress() {
fetch('/neverPress')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function lockEEPROM() {
fetch('/lockEEPROM')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function updateVariables() {
fetch('/variables')
.then(response => response.json())
.then(data => {
document.getElementById('unixTime').innerText = data.unixTime;
document.getElementById('nistTime').innerText = data.nistTime;
document.getElementById('date').innerText = data.date;
document.getElementById('time').innerText = data.time;
document.getElementById('displayMicroSeconds').innerText = data.displayMicroSeconds;
document.getElementById('microChange').innerText = data.microChange;
document.getElementById('pulseDurationBetweenRisingEdges').innerText = data.pulseDurationBetweenRisingEdges;
document.getElementById('timeAdjust').innerText = data.timeAdjust;
document.getElementById('differenceTime').innerText = data.differenceTime;
document.getElementById('freeMemory').innerText = data.freeMemory;
document.getElementById('averagePDBRE').innerText = data.averagePDBRE;
document.getElementById('runtimeSeconds').innerText = data.runtimeSeconds;
document.getElementById('totalPDBRE').innerText = data.totalPDBRE;
document.getElementById('adjustmentsMade').innerText = data.adjustmentsMade;
document.getElementById('xtalFreq').innerText = data.xtalFreq;
document.getElementById('avgXtalFreq').innerText = data.avgXtalFreq;
document.getElementById('fault').innerText = data.fault;
document.getElementById('globalPulseCount').innerText = data.globalPulseCount;
document.getElementById('espRuntime').innerText = data.espRuntime;
document.getElementById('lastNistUpdate').innerText = data.lastNistUpdate;
document.getElementById('runtimeHours').innerText = data.runtimeHours;
document.getElementById('faultCode').innerText = data.faultCode;
document.getElementById('bootupTime').innerText = data.bootupTime;
document.getElementById('status').innerText = data.status;
document.getElementById('stackSize').innerText = data.stackSize;
document.getElementById('ntpRequests').innerText = data.ntpRequests;
document.getElementById('nistAdjuster').innerText = data.nistAdjuster;
document.getElementById('compensateVariable').innerText = data.compensateVariable;
document.getElementById('oneWayDelay').innerText = data.oneWayDelay;
});
}
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>Stratum1 UNIX:<br></strong> <span id="nistTime">-</span></div>
<div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
<div class="variable" style="color: orange;background: Teal;"><strong>Last Datum:<br></strong> <span id="lastNistUpdate">-</span></div>
<div class="variable"><strong>NTP Requests:<br></strong> <span id="ntpRequests">-</span></div>
<div class="variable" style="background: DarkOliveGreen;"><strong>Pulse Comp:<br></strong> <span id="nistAdjuster">-</span> s</div>
<div class="variable" style="background: DarkOliveGreen;"><strong>Tare:<br></strong> <span id="compensateVariable">-</span> s</div>
<div class="variable" style="background: DarkOliveGreen;"><strong>Offset (Seconds):<br></strong> <span id="differenceTime">-</span> s</div>
<div class="variable" style="background: SteelBlue;"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
<div class="variable" style="background: SteelBlue;"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
<div class="variable" style="background: SteelBlue;"><strong>xTal Faults:<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>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
<div class="variable"><strong>NTP Stack Size:<br></strong> <span id="stackSize">-</span> bytes</div>
<div class="variable"><strong>Network Delay:<br></strong> <span id="oneWayDelay">-</span> s</div>
<div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> bytes</div>
<span id="faultCode" style="display: none;">-</span>
</div>
<div class="serial-container"> Serial Output Console
<div id="serialOutput" class="serial-output"></div>
</div>
<script>
function addSerialMessage(message) {
document.getElementById('serialOutput').innerHTML = ''; // Clears the content
const serialOutput = document.getElementById('serialOutput');
const currentTime = new Date().toLocaleTimeString();
const faultCode = document.getElementById('faultCode').innerText; // Get faultCode
const newMessage = `<div> <span class="timestamp">UTC: [${currentTime}]</span><br>${message}<strong> ${faultCode} </strong></div>`;
serialOutput.innerHTML += newMessage;
// Scroll to the bottom
serialOutput.scrollTop = serialOutput.scrollHeight;
}
//trigger the function every 2 seconds to update the console
setInterval(() => {
addSerialMessage(" ");
}, 2000); // Every 2 seconds, add a new message
</script>
<div class="description">
<div class="admin-controls">
<p>Administrator Controls - Auth Use Only</p>
<div id="unlock-section">
<input type="password" id="admin-password" placeholder="Enter Password">
<button onclick="unlockControls()">Unlock Controls</button>
</div>
<div id="button-section" style="margin-top:20px;">
<div class="button-grid">
<button onclick="syncNTP()" disabled="">Master Sync</button>
<button onclick="microUP()" disabled="">Pulse Inject</button>
<button onclick="timeAdjustINC()" disabled="">mSADJDAYINC</button>
<button onclick="compensateDown()" disabled="">+ve Tare</button>
<button onclick="bufferReset()" disabled="">Fault Reset</button>
<button onclick="microDOWN()" disabled="">Pulse Extract</button>
<button onclick="timeAdjustDEC()" disabled="">mSADJDayDEC</button>
<button onclick="compensateUp()" disabled="">-ve Tare</button>
<button onclick="" disabled=""></button>
<button onclick="lockEEPROM()" disabled="">NTP/Save</button>
<button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;" ="" onclick="confirmAction('Restart All Services', neverPress)" disabled="">
Restart All Services
</button>
<button onclick="autoTare()" disabled="">Auto Tare</button>
</div>
</div>
</div>
<script>
// Original password-protected function (commented out)
//start comment here
async function hashPassword(password) {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, "0")).join("");
}
// Precomputed hash of password
const storedHash = "8254c329a92850f6d539dd376f4816ee2764517da5e0235514af433164480d7a";
async function unlockControls() {
const enteredPassword = document.getElementById("admin-password").value;
const enteredHash = await hashPassword(enteredPassword);
if (enteredHash === storedHash) {
const buttons = document.querySelectorAll("#button-section button");
buttons.forEach(button => {
button.disabled = false;
});
document.getElementById("unlock-section").style.display = "none";
} else {
alert("Password Incorrect");
}
} //end of commented out section
/*
//start comment to unlock without password
// New function to unlock controls without a password
function unlockControls() {
// Enable all buttons without requiring a password
const buttons = document.querySelectorAll("#button-section button");
buttons.forEach(button => {
button.disabled = false;
});
}
// Automatically unlock controls on page load
window.onload = unlockControls;
// end comment
*/
// Confirmation action function
function confirmAction(action, callback) {
if (confirm("Are you sure you want to " + action + "?")) {
callback();
}
}
</script>
</div>
<div class="description">
<h3>ElectroKev Atomic Clock Features</h3>
<div class="mainlinks">
<a href="https://electrokev.com/eacp-code-v53.8" target="_blank">[22/02/25] ** Source Code View ** v53.8</a><br>
<a href="https://electrokev.com/eacp-code-v54.1" target="_blank">[24/02/25] ** Source Code View ** v54.1</a>
<span style="color: red;"> * Latest Version *</span>
<p>
</p>
<iframe id="myIFrame" src="https://electrokev.com/wp-content/uploads/html/manual1.html" ="" loading="lazy" width="100%" style="border:none;"></iframe>
<script>
window.addEventListener("message", (event) => {
// Ensure the message is from the correct domain
if (event.origin !== "https://electrokev.com") return;
if (event.data.iframeHeight) {
document.getElementById("myIFrame").style.height = event.data.iframeHeight + "px";
}
});
</script>
</div>
</div>
<div class="footer">
<a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
© Kevin Davy 2025
</div>
<br><br> Last Code Upload: )rawliteral"
+ String(buildDate) + " at " + String(buildTime) + R"rawliteral(
<p></p>
)rawliteral";
request->send(200, "text/html", index_html);
}</wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></espasyncwebserver.h></wifi.h></unixtime.h></liquidcrystal_i2c.h></wire.h></secrets.h>