/****************************************************************************
* ElectroKev Atomic Clock
* Author: Kevin Davy ([email protected])
* © 2025 - All Rights Reserved
*
* Written in C++, HTML, Javascript and CSS
* Hardware: ESP32 Dual Core Microcontroller
*
* 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 ESP32 ON CORE 1
RUN PULSEPROCESSBUFFER ON CORE 1
RUN NTP REQUEST SERVER ON 1 */
const float versionNumber = 53.2;
#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 = "pool.ntp.org"; // 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
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;
/* ──────────────────────────────────────────────────────────────── */
/* ⚠️ 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 = 10000; // Max characters stored in fault log
/* ──────────────────────────────────────────────────────────────── */
/* 🔄 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();
// --- Sync Time from NTP ---
syncWithNTP();
stamp.getDateTime(unix);
// --- Task: Process NTP Requests --- //start this back up
xTaskCreatePinnedToCore(
taskProcessNTPRequests, // Task function
"ProcessNTPRequests", // Task name
6096, // 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
6096, // Stack size
NULL, // Task parameters
4, // Priority (higher than pulse processing)
NULL, // Task handle
1 // Core
);
xTaskCreatePinnedToCore(
ntpRestartTask, // Task function
"NTP_Restart", // Task name
6048, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
0 // Core
);
xTaskCreatePinnedToCore(
taskUpdateDisplay, // Task function
"DisplayUpdate", // Task name
6048, // Stack size
NULL, // Task parameters
1, // Priority
NULL, // Task handle
0 // 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();
}
// --- 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(200)); // 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() {
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++;
// 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 1-minute tasks
if (runtimeOneMinute >= 60) {
nistUpdate();
lastNistUpdate = nistTime;
runtimeOneMinute = 0;
}
// Reset after 1 hour
if (runtimeSeconds >= 3600) {
}
// Process accumulated pulses (every 10 pulses)
if (pulseCount >= 10) {
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();
timeAdjustCorrections();
// Update time and display once per successful 10-pulse cycle
updateTime();
checkDifference();
}
}
}
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() {
nistTime++;
differenceTime = (unix - nistTime)+compensateVariable;
//differenceTime = fabs(fmod(differenceTime, 1.0));
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);
if (abs(differenceTime) >= 2) {
flagTimeDrift = true;
faultHandler();
customMessage("Self Repair: In Progress...DISABLED");
//syncWithNTP();
return;
}
}
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.println("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.println(ntpFractionSend);
// 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++;
stamp.getDateTime(unix);
//checkDifference();
timeNow = "["+String(formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second))+"]";
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"] = unix;
doc["bootupTime"] = bootupTime;
doc["nistTime"] = nistTime;
doc["date"] = String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year);
doc["time"] = formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second);
doc["displayMicroSeconds"] = displayMicroSeconds;
doc["microChange"] = microChange;
doc["pulseDurationBetweenRisingEdges"] = pulseDurationBetweenRisingEdges;
doc["timeAdjust"] = timeAdjust;
doc["differenceTime"] = 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"] = lastNistUpdate;
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);
// 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.println("NIST Update");
// Clear the buffer before sending the request
memset(ntpPacketBuffer, 0, NTP_PACKET_SIZE);
// Send an NTP request
sendNTPpacket(ntpServer);
vTaskDelay(pdMS_TO_TICKS(500));
if (ntpUDP.parsePacket()) {
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];
uint32_t unixSeconds = ntpSeconds - 2208988800UL;
double microseconds = (ntpFraction * 1000000.0) / 4294967296.0;
nistTime = ((unixSeconds+1) + (microseconds / 1000000.0));
lastNistUpdate = nistTime;
} else {
Serial.println("Failed to NIST UPDATE.");
}
NTPON=false;
}
void syncWithNTP() {
NTPON=true;
visitorsCount = 0;
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);
vTaskDelay(pdMS_TO_TICKS(500)); // Wait for response
if (ntpUDP.parsePacket()) {
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
checkDifference(); // Call the function to compare time against reference
nistTime = ((unixSeconds+1) + (microseconds / 1000000.0));
unix = (unixSeconds+1) + (microseconds / 1000000.0);
lastNistUpdate = nistTime;
bootupTime = unix;
Serial.print("NTP Sync Successful: ");
Serial.println(nistTime, 6); // Print with 6 decimal places
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;
pulseCount=0;
}
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(){
lcd.clear();
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");
});
}
void handleRoot(AsyncWebServerRequest *request) {
// Increment visitor count on each webpage load
clientIP=request->client()->remoteIP().toString();
visitorsCount++;
flagVisitor=true;
faultHandler();
String html = R"rawliteral(
<meta charset="UTF-8">
<title>ElectroKev Atomic Clock EACP</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1f1f1f, #333);
color: #e0e0e0;
font-family: 'Arial', sans-serif;
}
.time {
font-size: 4rem;
font-weight: bold;
text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
margin-bottom: 20px;
color: #00ff00;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #222;
color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 800px;
margin: 20px auto;
text-align: center;
width: 100%;
}
.variables {
display: grid;
grid-template-columns: repeat(3, 2fr);
gap: 10px;
width: 100%;
max-width: 800px;
text-align: left;
padding: 5px;
}
.variable {
font-size: 1rem;
padding: 12px 20px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.version-container {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
font-size: 1.1rem;
}
.version {
color: #fff;
font-weight: bold;
}
.last-updated {
color: #00ff00;
font-weight: normal;
}
.ntp {
color: orange;
font-weight: normal;
}
.footer {
margin-top: 30px;
font-size: 1.2rem;
text-align: center;
color: #ccc;
}
.footer a,
.mainlinks a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.description {
font-size: 1.2rem;
text-align: left;
color: #e0e0e0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
margin-top: 30px;
width: 100%;
max-width: 800px;
}
.description h3 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
}
.admin-controls {
text-align: center;
margin: 20px;
}
.serial-container {
margin-top: 30px;
width: 100%;
max-width: 800px;
height: 300px;
background-color: #1e1e1e;
color: #00ff00;
border-radius: 10px;
padding: 10px;
overflow-y: scroll;
box-shadow: 0px 0px 15px rgba(0, 255, 0, 0.1);
font-size: 22px;
white-space: pre-wrap;
}
.serial-output {
font-family: 'Courier New', monospace;
font-size: 10px;
margin: 0;
padding: 0;
line-height: 1.5;
}
.serial-output div {
padding: 2px;
}
.timestamps {
color: #ffdd00;
font-weight: bold;
}
.header {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.header2 {
font-size: 16px;
font-weight: normal;
color: #ffa500;
}
.subheader {
font-size: 16px;
color: #ccc;
margin-bottom: 15px;
}
.version-container,
.ntp-container,
.bootup-container {
margin-top: 10px;
font-size: 16px;
}
.status-label {
font-weight: bold;
margin-left: 10px; /*this one for main serial output */
color: #ffffff;
}
.status {
font-weight: bold;
}
.status[data-status="OK"] {
color: #00ff00;
}
.status[data-status="FAULT"] {
color: #ff0000;
}
/* Responsive Adjustments */
@media (max-width: 600px) {
.time { font-size: 6rem; }
.variable { font-size: 14px; }
.header, .header2 { font-size: 1rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
@media (max-width: 1024px) {
.time { font-size: 6rem; }
.variable { font-size: 24px; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
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 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;
});
}
setInterval(updateVariables, 500);
</script>
<div class="container">
<div class="header">
EACP<br>
<span class="header2">(ElectroKev Atomic Clock Project)</span>
</div>
<div class="subheader">Developed by <strong>Kevin Davy</strong> | January 2025</div>
<div class="version-container">
<span id="version" class="version">Loading version...</span>
<span id="lastUpdated" class="last-updated">
Last Code Upload: )rawliteral" + String(buildDate) + " at " + String(buildTime) + R"rawliteral(
</span>
</div>
<div class="ntp-container">
<span class="version">NTP Server:</span>
<span id="ntp" class="ntp">clock.electrokev.com Port 123</span>
<span class="status-label">Status:</span>
<span id="status" class="status">-</span>
</div>
<div class="bootup-container">Bootup Time: <span id="bootupTime"></span></div>
<script>
function updateVersion() {
fetch('/getVersion')
.then(response => response.json())
.then(data => {
document.getElementById('version').innerText = "Code Version " + data.version;
});
}
updateVersion();
</script>
</div>
<div class="variables">
<div class="variable" style="color: turquoise;background: Teal;"><strong>EACP UNIX:<br></strong> <span id="unixTime">-</span></div>
<div class="variable" style="color: White;background: Teal;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
<div class="variable" style="color: yellow;background: Teal;"><strong>NIST UNIX:<br></strong> <span id="nistTime">-</span></div>
<div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
<div class="variable" style="color: orange;background: Teal;"><strong>NIST Update:<br></strong> <span id="lastNistUpdate">-</span></div>
<div class="variable"><strong>Drift (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>Stack Size:<br></strong> <span id="stackSize">-</span> bytes</div>
<div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> bytes</div>
<div class="variable"><strong>NTP Requests:<br></strong> <span id="ntpRequests">-</span></div>
<div class="variable"><strong>NIST Adjuster:<br></strong> <span id="nistAdjuster">-</span> s</div>
<div class="variable"><strong>Compensation:<br></strong> <span id="compensateVariable">-</span> s</div>
<div class="variable"><strong><br></strong></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="">Sync with NTP</button>
<button onclick="microUP()" disabled="">microUP</button>
<button onclick="timeAdjustINC()" disabled="">mSADJDAYINC</button>
<button onclick="lockEEPROM()" disabled="">NTP/Save</button>
<button onclick="bufferReset()" disabled="">Fault Reset</button>
<button onclick="microDOWN()" disabled="">microDOWN</button>
<button onclick="timeAdjustDEC()" disabled="">mSADJDayDEC</button>
<button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;" ="" onclick="confirmAction('Never Press this Button!', neverPress)" disabled="">
NEVER Press this Button
</button>
<button onclick="" disabled=""></button>
<button onclick="compensateUp()" disabled="">Pos Compensate</button>
<button onclick="compensateDown()" disabled="">Neg Compensate</button>
<button onclick="" disabled=""></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-v52.8" target="_blank">[15/02/25] ** Source Code View ** v52.8</a><br>
<a href="https://electrokev.com/eacp-code-v53.1" target="_blank">[16/02/25] ** Source Code View ** v53.1</a><br>
<a href="https://electrokev.com/eacp-code-v53.2" target="_blank">[17/02/25] ** Source Code View ** v53.2</a>
<span style="color: red;"> * Latest Version *</span>
</div>
<p>The <strong>ElectroKev Atomic Clock</strong> incorporates advanced pulse monitoring and modulation technology, with precise synchronization and real-time correction of the time signal, ensuring microsecond-level accuracy. By tracking pulse drift from an oven-controlled OCXO 10 MHz Crystal, the clock dynamically compensates for drift, maintaining highly accurate timekeeping over extended periods.</p>
<h4>Key Features:</h4>
<ul>
<li><strong>Advanced Pulse Monitoring:</strong> The 10 MHz crystal is scaled by factors of 10 using 6 layers of the 74LS90 integrated circuit, creating a final 10 Hz input. The system continuously monitors and adjusts pulse signals to ensure precise timing.</li>
<li><strong>Real-Time Drift Compensation:</strong> Compensates for long-term drift with microsecond-level accuracy, ensuring stable timekeeping.</li>
<li><strong>User-Controlled Adjustments:</strong> Allows fine-tuning of pulse skipping and drift compensation to achieve optimal accuracy.</li>
<li><strong>Multi-LCD Display System:</strong> Provides detailed data on time, pulse handling, drift calculations, and temperature.</li>
<li><strong>Temperature Monitoring:</strong> Offers real-time temperature data with high precision and alerts for sensor malfunctions.</li>
<li><strong>Output Signal Control:</strong> Generates a modulated output signal with precise timing adjustments for synchronization.</li>
<li><strong>Rotary Encoder Integration:</strong> Enables precise adjustments to pulse modulation intervals with high resolution.</li>
<li><strong>Modular Design:</strong> Easily customizable to integrate additional sensors and outputs, enhancing functionality.</li>
<li><strong>Twin stabilised power supplies:</strong> The system uses twin power supplies, both stabilised and voltage and current regulated for accurate crystal oscillation.</li>
</ul>
<p>
Further Details:
</p>
<p>
The clock is an advanced, pulse-modulated timekeeping system that achieves exceptional precision by synchronizing with an external oven controlled crystal outputting a 10 MHz pulse. This system is designed to manipulate pulses with a remarkable accuracy of up to 1 microsecond per 24 hours, ensuring precise timekeeping over extended periods. It uses a segregated NIST time reference for synchronization, allowing it to maintain accurate time based on trusted atomic standards.
</p>
<p>
For manual adjustments, the clock offers controls to modify the time and date, allowing for fine-tuning through pulse injection or removal. A rotary encoder is used to adjust the daily time alteration rate, with the system automatically calculating the microseconds per pulse to ensure precision. This feature makes it adaptable for environments requiring high-precision time adjustments.
</p>
<p>
The clock is equipped with WiFi connectivity, enabling remote monitoring and control through a web application. This interface provides full access to all major clock functions, allowing users to interact with the system easily via server handlers. Continuous crystal speed monitoring helps identify and debug drift, ensuring the clock remains highly accurate.
</p>
<p>
In terms of fault detection, the clock is designed to identify invalid pulse readings automatically. It removes faulty pulses and logs debug data for analysis, making it resilient to errors. Additionally, the power supply is finely tuned, with regulated power sources dedicated to different components to ensure stable performance.
</p>
<p>
Firmware updates are handled over the air (OTA), ensuring the clock remains up-to-date without requiring manual intervention. The clock operates with a dual-core configuration, separating timekeeping functions from resource-intensive processes to optimize performance. Pulse monitoring inputs are prioritized through high-level processor interrupts for precise counting.
</p>
<p>
The clock also functions as a globally accessible NTP time server on port 123, providing highly accurate time synchronization. A web-based serial console is available for real-time debugging, with self-monitoring and error reporting capabilities to facilitate automatic correction of any issues.
</p>
</div>
<div class="footer">
<a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
© Kevin Davy 2025
</div>
<br><br> Last Code Upload: )rawliteral"
+ String(buildDate) + " at " + String(buildTime) + R"rawliteral(
<p></p>
)rawliteral";
request->send(200, "text/html", html);
}</wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></espasyncwebserver.h></wifi.h></unixtime.h></liquidcrystal_i2c.h></wire.h></secrets.h>