/****************************************************************************
* ElectroKev Atomic Clock
* Author: Kevin Davy ([email protected])
* © 2025 - All Rights Reserved
*
* Description:
* - Pulse-modulated clock synchronized to an external 10 MHz pulse.
* - Achieves fine pulse manipulation with accuracy up to 1 microsecond
* per 24 hours for precise timekeeping.
* - Segregated NIST time reference for synchronization.
* - Manual controls for time and date with pulse injection or removal.
* - Rotary encoder used to adjust daily time alteration rate,
* with automatic calculation for microseconds per pulse.
* - WiFi connectivity enabled for remote monitoring and control.
* - Continuous crystal speed monitoring to debug drift.
* - Web interface providing full control over all major clock functions
* via server handlers.
* - Advanced fault detection for invalid pulse readings,
* with automatic pulse removal and logging of debug data.
* - Regulated and finely tuned power design with individual power supplies
* for stable performance.
* - OTA updates for seamless firmware updates over the air.
* - Dual-core configuration with timekeeping processes separate from other
* resource-intensive functions for enhanced performance.
* - Pulse monitoring inputs configured on high-priority processor interrupts
* for accurate counting.
* - Globally accessible NTP time server running on port 123,
* providing highly accurate time synchronization.
* - Web based serial console with self monitoring error reporting.
* providing real time debugging and self correction.
*
***************************************************************************/
/* RUN EVENTS ON CORE 1
RUN ARDUINO ON CORE 1 //changed to core 0 due to Watchdog failure
RUN PULSEPROCESSBUFFER ON CORE 0
RUN ISR ON {PROCESSOR DECIDES BUT SET TO 1} */
const float versionNumber = 51.0;
#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <eeprom.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include "freertos/task.h"
#include "esp_bt.h"
#include <esp_system.h>
#include <esp_task_wdt.h>
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.h>
#include <wifiudp.h>
#include <ticker.h>
//#include "driver/gpio.h"
//network stuff
const char *ssid = SSID;
const char *password = PASSWORD;
const char *buildDate = __DATE__;
const char *buildTime = __TIME__;
// Set the static IP details
IPAddress local_ip(192, 168, 1, 172); //local ip
IPAddress gateway(192, 168, 1, 1); // gatewat
IPAddress subnet(255, 255, 255, 0); // subnet
AsyncWebServer server(8080);
Preferences preferences;
// --- NTP Configuration ---
const char *ntpServer = "pool.ntp.org"; // NTP server
const long gmtOffset_sec = 0; // Offset for UTC
const int daylightOffset_sec = 0; // No daylight savings time
WiFiUDP ntpUDP;
const int NTP_PORT =123;
// --- Pin Configurations ---
#define ROTARY_CLK 26 // Rotary encoder clock pin
#define ROTARY_DT 27 // Rotary encoder data pin
#define ROTARY_SW 13 // Rotary encoder switch pin
#define adjustHoursPin 16 // Button pin for adjusting hours
#define adjustMinutesPin 17 // Button pin for adjusting minutes
#define adjustSecondsPin 5 // Button pin for adjusting seconds
#define adjustMicroSecondUp 18 // Button pin for increasing microseconds
#define adjustMicroSecondDown 19 // Button pin for decreasing microseconds
#define syncNTC 23
#define pulsePin 12 // Pulse pin input for counting pulses
#define BUFFER_SIZE 256
// --- Global Objects ---
UnixTime stamp(0);
LiquidCrystal_I2C lcd(0x27, 20, 4);
// --- Time Variables ---
double microSeconds = 0;
double displayMicroSeconds;
double microChange = 0;
float rotaryChangeValue = 0.1; // Rotary encoder adjustment step
float timeAdjust = 0;
double pulseDurationBetweenRisingEdges; //changed from float
double invalidPulse;
double averagePDBRE; //changedfrom float
double totalPDBRE;
double totalPDBREAC=100000; //changed from float
long long espRuntime=0; //long to double
long runtimeSeconds = 0; //long to double
long runtimeHours=0;
const int crystalCorrection = 0; // Crystal correction factor
int adjustmentsMade;
double xtalFreq; //float to double
double totalXtalFreq; //float to double
double avgXtalFreq; //float to double
int lastUpdate = 0;
String timeNow;
//fault variables
int fault = 0;
int faultTime;
bool generalFault=true;
String status="FAULT";
String faultCode ="Fault: Unexpected ReStart ";
double faultyPDBRE;
bool flagInvalidPulse;
bool flagTimeDrift;
bool flagUnexpectedRestart=true;
bool flagNTPReceived;
bool flagNTPSync;
bool flagVisitor;
bool flagStackSize;
// --- Circular Buffer Variables ---
//volatile double currentTime;
volatile double currentTime; // uint to double
volatile double nextHead; // uint to double
volatile double pulseTimes[BUFFER_SIZE]; // was double then long now int
volatile uint32_t bufferHead = 0;
volatile uint32_t bufferTail = 0;
double lastRisingEdgeTime = 0;
int pulseCount = 0;
double globalPulseCount = 0;
// --- Date Setting ---
bool dateSetting = 0;
// --- Time Variables ---
int32_t nistTime;
int32_t unix;
double differenceTime; //int to double
double lastNistUpdate;
double bootupTime;
// --- General Variables ---
int visitorsCount;
int freeMemory = 0;
String clientIP;
double stackSize;
// --- Interrupt Service Routine (ISR) ---
void IRAM_ATTR pulseISR() {
/* int coreID = xPortGetCoreID(); // Returns 0 for Core 0, 1 for Core 1
Serial.print("ISR on Core: ");
Serial.println(coreID);*/
currentTime=esp_timer_get_time();
nextHead = (bufferHead + 1) % BUFFER_SIZE;
if (nextHead != bufferTail) { // Check if the buffer is not full
pulseTimes[bufferHead] = currentTime;
bufferHead = nextHead;
}
}
// --- Setup Function ---
void setup() {
//Shut down BT
Serial.begin(115200);
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
esp_bt_controller_disable();
esp_bt_controller_deinit();
Serial.println("Bluetooth disabled.");
} else {
Serial.println("Bluetooth was already disabled.");
}
// Setup WiFi connection
setupWifi(); // Starts WiFi and web server (which runs on Core 0)
freeMemory = ESP.getFreeHeap();
Serial.println("Starting OTA...");
ArduinoOTA.begin();
Serial.println("OTA Ready!");
ArduinoOTA.setPassword(nullptr); // Disable password
// --- Rotary Encoder Pins ---
pinMode(ROTARY_CLK, INPUT);
pinMode(ROTARY_DT, INPUT);
pinMode(ROTARY_SW, INPUT_PULLUP);
// --- Pulse Pin Setup ---
pinMode(pulsePin, INPUT);
// --- LCD Initialization ---
lcd.init();
lcd.begin(20, 4);
lcd.backlight();
// --- Button Pins Setup ---
pinMode(adjustHoursPin, INPUT_PULLUP);
pinMode(adjustMinutesPin, INPUT_PULLUP);
pinMode(adjustSecondsPin, INPUT_PULLUP);
pinMode(adjustMicroSecondDown, INPUT_PULLUP);
pinMode(adjustMicroSecondUp, INPUT_PULLUP);
pinMode(ROTARY_SW, INPUT_PULLUP);
pinMode(syncNTC, INPUT_PULLUP);
loadFromEEPROM();
// --- Set Date and Time from Unix Timestamp ---
syncWithNTP();
stamp.getDateTime(unix);
attachInterrupt(digitalPinToInterrupt(pulsePin), pulseISR, FALLING);
// Create the task for processing the pulse buffer
xTaskCreatePinnedToCore(
[](void*) {
while (true) {
processPulseBuffer(); // This function runs in a separate task
vTaskDelay(15); // Delay to give CPU time to other tasks, adjust if needed
}
},
"processPulseBuffer", // Task name
4096, // Stack size (adjust if needed)
NULL, // No parameters
4, // Priority (set according to your needs)
NULL, // Task handle
0 // Pin to Core 0 (or Core 0 depending on your setup)
);
xTaskCreatePinnedToCore(
[](void*){
pulseISR();
vTaskDelete(NULL); // Delete task when done
},
"pulseISR", // Task name
4096, // Stack size (adjust if needed)
NULL, // No parameters
10, // Priority (set according to your needs)
NULL, // Task handle
1 // Pin to Core 0
);
Serial.println("Running");
flagUnexpectedRestart=true;
faultHandler();
}
// --- Main Loop ---
void loop() {
//processPulseBuffer(); // Process pulses from the buffer
//handleButtons(); // Check and process button inputs
//handleRotaryEncoder(); // Handle rotary encoder inputs
adjustmentRoutine(); // Adjust time if microseconds overflow
ArduinoOTA.handle();
processNTPRequests();
faultCheck();
vTaskDelay(10);
}
void faultCheck(){
generalFault = flagUnexpectedRestart || flagTimeDrift || flagInvalidPulse; // Set generalFault
status = generalFault ? "FAULT" : "OK"; // Update status
}
void faultHandler() {
generalFault = false; // Reset before checking flags
// Handle unexpected restart
if (flagUnexpectedRestart) {
esp_reset_reason_t resetReason = esp_reset_reason();
String resetReasonText;
switch (resetReason) {
case ESP_RST_POWERON: resetReasonText = "POWERON"; break;
case ESP_RST_BROWNOUT: resetReasonText = "BROWNOUT"; break;
case ESP_RST_SW: resetReasonText = "SW"; break;
case ESP_RST_EXT: resetReasonText = "EXT"; break;
case ESP_RST_PANIC: resetReasonText = "MEMPANIC"; break;
case ESP_RST_WDT: resetReasonText = "WATCHDOG"; break;
default: resetReasonText = "UNKNOWN"; break;
}
faultCode += timeNow+ " Fault: Unexpected Restart ESP_RST_" + resetReasonText + " " + String(espRuntime)+" Sec "+"\n";
generalFault = true;
}
// Handle specific faults
if (flagTimeDrift) {
faultCode += timeNow+ " Fault: Time Drift -> " + String(differenceTime) + "\n";
generalFault = true;
flagTimeDrift=false;
}
if (flagInvalidPulse) {
faultCode += timeNow +". Fault: Invalid Pulse (" + String(invalidPulse) + "\n";
generalFault = true;
}
// Log events (without triggering generalFault)
if (flagNTPReceived) {
faultCode += timeNow+" NTP Request Received " + "\n";
flagNTPReceived = false;
}
if (flagNTPSync) {
faultCode += timeNow +" NTP Successfully Synced " + "\n";
flagNTPSync = false;
}
if (flagVisitor) {
faultCode += timeNow + " Web User Logged In [IP:" + clientIP +"]" + "\n";
flagVisitor = false;
}
if (flagStackSize) {
faultCode += timeNow+" StackSize Remaining:" + String(stackSize) +" bytes "+ "\n";
flagStackSize = false;
}
// Save fault status and update system status
saveToEEPROM();
status = generalFault ? "FAULT" : "OK";
}
IRAM_ATTR void processPulseBuffer() {
//int coreID = xPortGetCoreID(); // Returns 0 for Core 0, 1 for Core 1
// Serial.print("Pulse Processor on Core: ");
// Serial.println(coreID);
static unsigned long lastMillis = millis(); // Track last update time for nistTime
while (bufferTail != bufferHead) {
// Disable interrupts temporarily to process the pulse
noInterrupts();
double pulseTime = pulseTimes[bufferTail];
bufferTail = (bufferTail + 1) % BUFFER_SIZE;
interrupts(); // Re-enable interrupts
pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime) + crystalCorrection;
lastRisingEdgeTime = pulseTime;
// Initialize variables at startup
if (runtimeSeconds == 0) {
globalPulseCount = 9;
totalXtalFreq = 90;
fault = 0;
}
// Validate the pulse duration
if (pulseDurationBetweenRisingEdges >= 50000 && pulseDurationBetweenRisingEdges <= 150000) {
pulseCount++;
} else {
invalidPulse=pulseDurationBetweenRisingEdges;
fault++;
flagInvalidPulse = true;
faultHandler();
}
// Calculate crystal frequency
xtalFreq = 1000000 / pulseDurationBetweenRisingEdges;
// Update crystal frequency tracking
if (runtimeSeconds > 0) {
totalXtalFreq += xtalFreq;
avgXtalFreq = totalXtalFreq / globalPulseCount;
}
globalPulseCount++;
// Perform 1-minute tasks
if (espRuntime % 60 == 0) {
nistUpdate();
lastNistUpdate = nistTime;
}
// Reset after 1 hour
if (runtimeSeconds >= 3600) {
if(stackSize<2000){
flagStackSize=true;
faultHandler();
}
totalPDBREAC=0;
totalPDBRE = 100000;
runtimeSeconds = 0;
runtimeHours++;
averagePDBRE = 0;
avgXtalFreq = 0;
totalXtalFreq = 0;
globalPulseCount = 0;
freeMemory = ESP.getFreeHeap();
}
//displayVariables();
// Process accumulated pulses (every 10 pulses)
if (pulseCount >= 10) {
if (runtimeSeconds > 0) {
totalPDBREAC += pulseDurationBetweenRisingEdges; // Update total pulse duration
totalPDBRE = ((runtimeSeconds + 1) * 100000) - totalPDBREAC;
averagePDBRE = totalPDBREAC / (runtimeSeconds + 1); // Long-term average pulse duration
}
runtimeSeconds++;
pulseCount = 0;
microSeconds += (microChange * 10); // Adjust microseconds based on pulse count
// Adjust display for remaining microseconds per hour
if (timeAdjust != 0) {
if (microSeconds < 0) {
displayMicroSeconds = round((100000 + microSeconds) / (10 * abs(microChange) * 3600) * 1000) / 1000;
} else if (microSeconds > 0) {
displayMicroSeconds = round((100000 - microSeconds) / (10 * microChange * 3600) * 1000) / 1000;
}
// Update microseconds adjustment
microChange = ((timeAdjust * 1000000) / 864000);
// Correct very small timeAdjust values close to 0
if (abs(timeAdjust) < 0.1) {
timeAdjust = 0.0;
microSeconds = 0;
microChange = 0;
displayMicroSeconds = 0;
}
}
// Update time and display once per successful 10-pulse cycle
updateTime();
updateDisplay();
}
// Increment `nistTime` every 1000 milliseconds
if (millis() - lastMillis >= 1000) {
differenceTime = unix - nistTime;
nistTime++;
stackSize=(uxTaskGetStackHighWaterMark(NULL));
// Set fault code if time sync drifts beyond limit
if (differenceTime >= 3 || differenceTime <= -3) {
flagTimeDrift = true;
faultHandler();
syncWithNTP();
}
lastMillis += 1000;
espRuntime++;
}
}
}
void processNTPRequests() {
int packetSize = ntpUDP.parsePacket();
if (packetSize) {
Serial.println("NTP request received");
byte packetBuffer[48]; // NTP request packet
ntpUDP.read(packetBuffer, packetSize); // Read request
// Set up the NTP response structure
memset(packetBuffer, 0, 48);
packetBuffer[0] = 0b00100100; // LI, Version, Mode
// Get current Unix time from your OCXO-based clock
unsigned long currentEpoch = unix;
// Convert to NTP timestamp format (seconds since 1900)
unsigned long ntpSeconds = currentEpoch + 2208988800UL;
packetBuffer[40] = (ntpSeconds >> 24) & 0xFF;
packetBuffer[41] = (ntpSeconds >> 16) & 0xFF;
packetBuffer[42] = (ntpSeconds >> 8) & 0xFF;
packetBuffer[43] = ntpSeconds & 0xFF;
// Send response
ntpUDP.beginPacket(ntpUDP.remoteIP(), ntpUDP.remotePort());
ntpUDP.write(packetBuffer, 48);
ntpUDP.endPacket();
Serial.println("NTP response sent");
flagNTPReceived=true;
faultHandler();
}
}
/// --- Handle Rotary Encoder Inputs ---
void handleRotaryEncoder() {
static int lastEncoderState = HIGH;
int currentEncoderState = digitalRead(ROTARY_CLK);
static unsigned long lastEncoderChangeTime = 0;
const unsigned long debounceDelay = 80; // Increased debounce delay
// Check for debounce timing
if ((millis() - lastEncoderChangeTime) > debounceDelay) {
if (currentEncoderState != lastEncoderState) {
// If the direction is different, increment or decrement the value by the desired rotaryChangeValue
if (digitalRead(ROTARY_DT) != currentEncoderState) {
timeAdjust += 0.1; // Adjust by 0.1
} else {
timeAdjust -= 0.1; // Adjust by -0.1
}
// Update the last change time
lastEncoderChangeTime = millis();
}
}
// Update the last encoder state
lastEncoderState = currentEncoderState;
}
// --- Handle Button Inputs ---
void handleButtons() {
static unsigned long lastButtonPressTime = 0;
const unsigned long buttonDebounceInterval = 150;
// Check for debounce interval
if (millis() - lastButtonPressTime > buttonDebounceInterval) {
// Toggle dateSetting if rotary switch is pressed
if (digitalRead(ROTARY_SW) == LOW) {
saveToEEPROM();
dateSetting = !dateSetting; // Toggle the value of dateSetting
}
// Adjust time or date based on dateSetting state
if (!dateSetting) {
// Time adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 3600; // Add one hour
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 60; // Add one minute
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix -= stamp.second; // Reset seconds
pulseCount = 0;
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
pulseCount--; // Decrease pulse count
} else if (digitalRead(adjustMicroSecondUp) == LOW) {
pulseCount++; // Increase pulse count
} else if (digitalRead(syncNTC) == LOW) {
lcd.clear();
lcd.print("WiFi Toggle Button Pressed");
if (WiFi.status() == WL_CONNECTED) {
lcd.print("Turning WiFi OFF...");
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
lcd.print("Turning WiFi ON...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
}
delay(500);
lcd.clear();
}
} else if (dateSetting) {
// Date adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 86400; // Add one day
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 2592000; // Add one month (needs refinement for proper month handling)
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix += 31622400; // Add one year
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
unix = 1736521548;
}
}
// Update date and time
stamp.getDateTime(unix);
// Update debounce timer
lastButtonPressTime = millis();
}
}
// --- Update Time ---
void updateTime() {
unix++;
stamp.getDateTime(unix);
timeNow = "["+String(formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second))+"]";
}
// --- Update Display ---
void updateDisplay() {
if (dateSetting) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Date Setting Mode");
lcd.setCursor(0, 1);
lcd.print("Adj:");
lcd.print(timeAdjust, 1);
lcd.print(" ");
lcd.print(displayMicroSeconds); // Display positive microseconds
return;
}
// Display full time and date
lcd.setCursor(0, 0);
displayFullTimeDate();
// Display UTC time
lcd.setCursor(0, 1);
lcd.print("UTC: ");
lcd.print(unix),0; // Display Unix timestamp
lcd.setCursor(0, 2);
lcd.print("NTP: ");
lcd.print(nistTime),0;
// Display calculated crystal frequency
lcd.setCursor(0, 3);
lcd.print(avgXtalFreq, 6);
lcd.print(" MHz ");
// Display pulse adjustments
lcd.setCursor(14, 3);
lcd.print("PA:");
lcd.print(adjustmentsMade);
}
// --- Adjustment Routine ---
void adjustmentRoutine() {
if (microSeconds >= 100000) {
microSeconds = 0;
pulseCount++;
adjustmentsMade++;
} else if (microSeconds <= -100000) {
microSeconds = 0;
pulseCount--;
adjustmentsMade++;
}
}
void loadFromEEPROM() {
preferences.begin("clock", true); // Open in read-only mode
microChange = preferences.getInt("microChange", 0); // Default value 0
timeAdjust = preferences.getFloat("timeAdjust", 0);
faultCode = preferences.getString("faultCode", faultCode);
//unix = preferences.getLong("unix", 0);
//visitorsCount = preferences.getInt("visitorsCount", 0);
runtimeHours = preferences.getDouble("runtimeHours", 0);
preferences.end();
}
void saveToEEPROM() {
preferences.begin("clock", false); // Open a namespace (false = read/write mode)
preferences.putInt("microChange", microChange);
preferences.putString("faultCode", faultCode);
preferences.putFloat("timeAdjust", timeAdjust);
//preferences.putLong("unix", unix);
//preferences.putInt("visitorsCount", visitorsCount);
preferences.putDouble("runtimeHours", runtimeHours);
preferences.end(); // Close the Preferences storage
}
// --- Format Time ---
String formatTime(int timeValue) {
return (timeValue < 10) ? "0" + String(timeValue) : String(timeValue);
}
void displayFullTimeDate() {
// Get the current time and date from the Unix timestamp
int currentYear = (stamp.year); // Current year
int currentMonth = (stamp.month); // Current month (1-12)
int currentDay = (stamp.day); // Current day
int currentHour = (stamp.hour); // Current hour
int currentMinute = (stamp.minute); // Current minute
int currentSecond = (stamp.second); // Current second
// Display time and date on the LCD
lcd.print(currentDay);
lcd.print("/");
lcd.print(currentMonth);
lcd.print("/");
lcd.print(currentYear);
// lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(formatTime(currentHour));
lcd.print(":");
lcd.print(formatTime(currentMinute));
lcd.print(":");
lcd.print(formatTime(currentSecond));
}
// --- Display Variables in Serial Monitor ---
void displayVariables() {
Serial.print(" RT: ");
Serial.print(runtimeSeconds);
Serial.print(" PC: ");
Serial.print(pulseCount);
Serial.print(" GPC: ");
Serial.print(globalPulseCount);
Serial.print(" PDBRE: ");
Serial.print(pulseDurationBetweenRisingEdges);
Serial.print(" unix: ");
Serial.print(unix);
Serial.print(" nistTime: ");
Serial.print(nistTime);
Serial.print(" dT: ");
Serial.print(differenceTime);
Serial.print(" xTalFreq: ");
Serial.print(xtalFreq, 6);
Serial.print(" totalXF: ");
Serial.print(totalXtalFreq, 6);
Serial.print(" avgXF: ");
Serial.println(avgXtalFreq, 6);
}
void setupWifi() {
// Begin WiFi connection
WiFi.begin(ssid, password);
// Wait for the WiFi connection
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
// Display WiFi connection details
Serial.println("Connected to WiFi");
Serial.println(WiFi.localIP());
ntpUDP.begin(NTP_PORT);
Serial.println("NTP Server started on port 123");
// Set up route handlers
routeHandlers();
// Start the server
server.begin();
WiFi.setSleep(false);
// Server started message
Serial.println("Server started!");
}
void handleVariables(AsyncWebServerRequest *request) {
// Create a JSON document (this is a fixed-size buffer to hold the JSON)
StaticJsonDocument<1024> doc;
// Fill the JSON document with data
doc["unixTime"] = unix;
doc["bootupTime"] = bootupTime;
doc["nistTime"] = nistTime;
doc["date"] = String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year);
doc["time"] = formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second);
doc["displayMicroSeconds"] = displayMicroSeconds;
doc["microChange"] = microChange;
doc["pulseDurationBetweenRisingEdges"] = pulseDurationBetweenRisingEdges;
doc["timeAdjust"] = timeAdjust;
doc["differenceTime"] = differenceTime;
doc["freeMemory"] = freeMemory;
doc["averagePDBRE"] = String(averagePDBRE, 2);
doc["runtimeSeconds"] = runtimeSeconds;
doc["adjustmentsMade"] = adjustmentsMade;
doc["xtalFreq"] = String(xtalFreq, 6);
doc["avgXtalFreq"] = String(avgXtalFreq, 6);
doc["fault"] = fault;
doc["globalPulseCount"] = globalPulseCount;
doc["totalPDBRE"] = totalPDBRE;
doc["espRuntime"] = espRuntime;
doc["lastNistUpdate"] = lastNistUpdate;
doc["runtimeHours"] = runtimeHours;
doc["faultCode"] = String(faultCode);
doc["status"] = String(status);
// Serialize the JSON document to a string
String json;
serializeJson(doc, json);
// Send the JSON response
request->send(200, "application/json", json);
}
void nistUpdate() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo2;
if (!getLocalTime(&timeinfo2)) {
return;
}
nistTime = (mktime(&timeinfo2));
}
void syncWithNTP() {
visitorsCount = 0;
// Configure NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Connecting to NTP server...");
// Wait for NTP sync
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 10000)) { // Wait for up to 10 seconds
Serial.println("Failed to get time from NTP server.");
lcd.clear();
lcd.print("NTP Sync Failed!");
return;
}
// Convert to Unix timestamp and update time
//unix = (mktime(&timeinfo))+ 1;
unix = (mktime(&timeinfo));
bootupTime=unix;
nistTime = (mktime(&timeinfo));
microSeconds = 0; // Reset microsecond adjustments
pulseCount = 0;
stamp.getDateTime(unix);
// Debug Output
Serial.println("NTP Sync Successful!");
flagNTPSync=true;
faultHandler();
Serial.print("Unix Time: ");
Serial.println(unix);
// Update LCD
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("NTP Sync OK!");
lcd.setCursor(0, 1);
lcd.print("Time Updated.");
}
void fullReset(){
generalFault=false;
fault = 0;
flagInvalidPulse=false;
flagUnexpectedRestart=false;
flagTimeDrift=false;
flagNTPReceived=false;
flagNTPSync=false;
flagVisitor=false;
flagStackSize=false;
faultCode=" Status: OK : StackSize = "+String(stackSize)+"\n";
totalPDBRE = 0;
totalPDBREAC=100000;
runtimeSeconds = 0;
espRuntime =0;
averagePDBRE = 0;
xtalFreq = 0;
avgXtalFreq = 0;
totalXtalFreq = 0;
globalPulseCount = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Full Reset");
saveToEEPROM();
}
void routeHandlers() {
// Define route handlers
server.on("/", HTTP_GET, handleRoot);
server.on("/variables", HTTP_GET, handleVariables);
// Synchronize time with NTP
server.on("/syncNTP", HTTP_GET, [](AsyncWebServerRequest *request) {
syncWithNTP();
request->send(200, "text/plain", "NTP Sync Completed");
});
// Increase pulse count
server.on("/microUP", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount++;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroUp");
request->send(200, "text/plain", "Pulse Count Increased");
});
// Decrease pulse count
server.on("/microDOWN", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount--;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroDown");
request->send(200, "text/plain", "Pulse Count Decreased");
});
// Increase time adjust
server.on("/timeAdjustINC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust += 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust INC");
request->send(200, "text/plain", "Time Adjust Increased");
});
// Decrease time adjust
server.on("/timeAdjustDEC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust -= 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust DEC");
request->send(200, "text/plain", "Time Adjust Decreased");
});
// Get version number
server.on("/getVersion", HTTP_GET, [](AsyncWebServerRequest *request) {
String version = String(versionNumber, 1);
request->send(200, "application/json", "{\"version\":\"" + version + "\"}");
});
// Lock data to EEPROM
server.on("/lockEEPROM", HTTP_GET, [](AsyncWebServerRequest *request) {
saveToEEPROM();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("EEPROM Saved");
request->send(200, "text/plain", "Saved to EEPROM");
});
// Reset buffer
server.on("/bufferReset", HTTP_GET, [](AsyncWebServerRequest *request) {
fullReset();
request->send(200, "text/plain", "Buffer Reset");
});
// Restart the ESP32
server.on("/neverPress", HTTP_GET, [](AsyncWebServerRequest *request) {
ESP.restart();
request->send(200, "text/plain", "Never Press");
});
}
void handleRoot(AsyncWebServerRequest *request) {
// Increment visitor count on each webpage load
clientIP=request->client()->remoteIP().toString();
visitorsCount++;
flagVisitor=true;
faultHandler();
String html = R"rawliteral(
<meta charset="UTF-8">
<title>ElectroKev Atomic Clock EACP</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1f1f1f, #333);
color: #e0e0e0;
font-family: 'Arial', sans-serif;
}
.time {
font-size: 4rem;
font-weight: bold;
text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
margin-bottom: 20px;
color: #00ff00;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #222;
color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 800px;
margin: 20px auto;
text-align: center;
width: 100%;
}
.variables {
display: grid;
grid-template-columns: repeat(3, 2fr);
gap: 10px;
width: 100%;
max-width: 800px;
text-align: left;
padding: 5px;
}
.variable {
font-size: 1rem;
padding: 12px 20px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.version-container {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
font-size: 1.1rem;
}
.version {
color: #fff;
font-weight: bold;
}
.last-updated {
color: #00ff00;
font-weight: normal;
}
.ntp {
color: orange;
font-weight: normal;
}
.footer {
margin-top: 30px;
font-size: 1.2rem;
text-align: center;
color: #ccc;
}
.footer a,
.mainlinks a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.description {
font-size: 1.2rem;
text-align: left;
color: #e0e0e0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
margin-top: 30px;
width: 100%;
max-width: 800px;
}
.description h3 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
}
.admin-controls {
text-align: center;
margin: 20px;
}
.serial-container {
margin-top: 30px;
width: 100%;
max-width: 800px;
height: 300px;
background-color: #1e1e1e;
color: #00ff00;
border-radius: 10px;
padding: 10px;
overflow-y: scroll;
box-shadow: 0px 0px 15px rgba(0, 255, 0, 0.1);
font-size: 22px;
white-space: pre-wrap;
}
.serial-output {
font-family: 'Courier New', monospace;
font-size: 10px;
margin: 0;
padding: 0;
line-height: 1.5;
}
.serial-output div {
padding: 2px;
}
.timestamps {
color: #ffdd00;
font-weight: bold;
}
.header {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.header2 {
font-size: 16px;
font-weight: normal;
color: #ffa500;
}
.subheader {
font-size: 16px;
color: #ccc;
margin-bottom: 15px;
}
.version-container,
.ntp-container,
.bootup-container {
margin-top: 10px;
font-size: 16px;
}
.status-label {
font-weight: bold;
margin-left: 10px; /*this one for main serial output */
color: #ffffff;
}
.status {
font-weight: bold;
}
.status[data-status="OK"] {
color: #00ff00;
}
.status[data-status="FAULT"] {
color: #ff0000;
}
/* Responsive Adjustments */
@media (max-width: 600px) {
.time { font-size: 6rem; }
.variable { font-size: 14px; }
.header, .header2 { font-size: 1rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
@media (max-width: 1024px) {
.time { font-size: 6rem; }
.variable { font-size: 24px; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
/* -------------------------------------------- */
/* Modern Button Styling - Editable Section */
/* -------------------------------------------- */
button {
font-family: 'Arial', sans-serif;
font-size: 0.8rem;
padding: 12px 20px;
border: none;
border-radius: 8px;
background: rgba(0, 255, 0, 0.2); /* Transparent green effect */
color: #00ff00;
cursor: pointer;
transition: all 0.3s ease-in-out;
backdrop-filter: blur(8px); /* Glass effect */
border: 1px solid rgba(0, 255, 0, 0.4);
box-shadow: 0 4px 10px rgba(0, 255, 0, 0.2);
text-transform: uppercase;
font-weight: bold;
letter-spacing: 1px;
width: 100%; /* Ensures uniform width */
max-width: 200px; /* Prevents stretching */
}
/* Hover Effect */
button:hover {
background: rgba(0, 255, 0, 0.3);
box-shadow: 0 6px 15px rgba(0, 255, 0, 0.4);
transform: translateY(-2px);
}
/* Active Button (When Clicked) */
button:active {
background: rgba(0, 255, 0, 0.4);
box-shadow: 0 2px 5px rgba(0, 255, 0, 0.5);
transform: scale(0.98);
}
/* Disabled Button */
button:disabled {
background: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: not-allowed;
box-shadow: none;
}
/* Focus Effect */
button:focus {
outline: none;
box-shadow: 0 0 8px rgba(0, 255, 0, 0.6);
}
/* Button Container & Grid */
.button-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
/* Fixed Grid for Buttons */
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); /* Ensures proper wrapping */
gap: 10px;
justify-items: center;
width: 100%; /* Makes sure the grid expands correctly */
max-width: 600px; /* Prevents excessive stretching */
margin: 0 auto; /* Centers the grid */
}
/* Mobile-Friendly Fix */
@media (max-width: 800px) {
button {
font-size: 1rem;
padding: 14px;
width: 100%;
max-width: 250px;
}
.button-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); /* Adjusts for smaller screens */
}
}
</style>
<script>
function syncNTP() {
fetch('/syncNTP')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function microDOWN() {
fetch('/microDOWN')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function microUP() {
fetch('/microUP')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustINC() {
fetch('/timeAdjustINC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustDEC() {
fetch('/timeAdjustDEC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function bufferReset() {
fetch('/bufferReset')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function neverPress() {
fetch('/neverPress')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function lockEEPROM() {
fetch('/lockEEPROM')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function updateVariables() {
fetch('/variables')
.then(response => response.json())
.then(data => {
document.getElementById('unixTime').innerText = data.unixTime;
document.getElementById('nistTime').innerText = data.nistTime;
document.getElementById('date').innerText = data.date;
document.getElementById('time').innerText = data.time;
document.getElementById('displayMicroSeconds').innerText = data.displayMicroSeconds;
document.getElementById('microChange').innerText = data.microChange;
document.getElementById('pulseDurationBetweenRisingEdges').innerText = data.pulseDurationBetweenRisingEdges;
document.getElementById('timeAdjust').innerText = data.timeAdjust;
document.getElementById('differenceTime').innerText = data.differenceTime;
document.getElementById('freeMemory').innerText = data.freeMemory;
document.getElementById('averagePDBRE').innerText = data.averagePDBRE;
document.getElementById('runtimeSeconds').innerText = data.runtimeSeconds;
document.getElementById('totalPDBRE').innerText = data.totalPDBRE;
document.getElementById('adjustmentsMade').innerText = data.adjustmentsMade;
document.getElementById('xtalFreq').innerText = data.xtalFreq;
document.getElementById('avgXtalFreq').innerText = data.avgXtalFreq;
document.getElementById('fault').innerText = data.fault;
document.getElementById('globalPulseCount').innerText = data.globalPulseCount;
document.getElementById('espRuntime').innerText = data.espRuntime;
document.getElementById('lastNistUpdate').innerText = data.lastNistUpdate;
document.getElementById('runtimeHours').innerText = data.runtimeHours;
document.getElementById('faultCode').innerText = data.faultCode;
document.getElementById('bootupTime').innerText = data.bootupTime;
document.getElementById('status').innerText = data.status;
});
}
setInterval(updateVariables, 500);
</script>
<div class="container">
<div class="header">
EACP<br>
<span class="header2">(ElectroKev Atomic Clock Project)</span>
</div>
<div class="subheader">Developed by <strong>Kevin Davy</strong> | January 2025</div>
<div class="version-container">
<span id="version" class="version">Loading version...</span>
<span id="lastUpdated" class="last-updated">
Last Code Upload: )rawliteral" + String(buildDate) + " at " + String(buildTime) + R"rawliteral(
</span>
</div>
<div class="ntp-container">
<span class="version">NTP Server:</span>
<span id="ntp" class="ntp">clock.electrokev.com Port 123</span>
<span class="status-label">Status:</span>
<span id="status" class="status">-</span>
</div>
<div class="bootup-container">Bootup Time: <span id="bootupTime"></span></div>
<script>
function updateVersion() {
fetch('/getVersion')
.then(response => response.json())
.then(data => {
document.getElementById('version').innerText = "Code Version " + data.version;
});
}
updateVersion();
</script>
</div>
<div class="variables">
<div class="variable" style="color: turquoise;background: Teal;"><strong>EACP UNIX:<br></strong> <span id="unixTime">-</span></div>
<div class="variable" style="color: White;background: Teal;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
<div class="variable" style="color: yellow;background: Teal;"><strong>NIST UNIX:<br></strong> <span id="nistTime">-</span></div>
<div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
<div class="variable" style="color: orange;background: Teal;"><strong>NIST Update:<br></strong> <span id="lastNistUpdate">-</span></div>
<div class="variable"><strong>Accuracy:<br></strong> <span id="differenceTime">-</span> seconds</div>
<div class="variable" style="background: SteelBlue;"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
<div class="variable" style="background: SteelBlue;"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
<div class="variable" style="background: SteelBlue;"><strong>xTal Faults:<br></strong> <span id="fault">-</span></div>
<div class="variable" style="background: Olive;"><strong>PDBRE:<br></strong> <span id="pulseDurationBetweenRisingEdges">-</span> μS</div>
<div class="variable" style="background: Olive;"><strong>Avg PDBRE:<br></strong> <span id="averagePDBRE">-</span> μs</div>
<div class="variable" style="background: Olive;"><strong>PDBRE Err:<br></strong> <span id="totalPDBRE">-</span> μs</div>
<div class="variable" style="background: Sienna;"><strong>Adjust:<br></strong> <span id="timeAdjust">-</span> s/Day</div>
<div class="variable" style="background: Sienna;"><strong>μs/Pulse+-:<br></strong> <span id="microChange">-</span></div>
<div class="variable" style="background: Sienna;"><strong>PulseAdj/Next:<br></strong> <span id="adjustmentsMade">-</span> / <span id="displayMicroSeconds">-</span> Hrs</div>
<div class="variable"><strong>RunTime (Sec/Hrs)<br></strong> <span id="runtimeSeconds">-</span> / <span id="runtimeHours">-</span></div>
<div class="variable"><strong>ESP RunTime<br></strong> <span id="espRuntime">-</span></div>
<div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> bytes</div>
<div class="variable"></div>
<div class="variable"><strong>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
<div class="variable"></div>
<span id="faultCode" style="display: none;">-</span>
</div>
<div class="serial-container"> Serial Output Console
<div id="serialOutput" class="serial-output"></div>
</div>
<script>
function addSerialMessage(message) {
document.getElementById('serialOutput').innerHTML = ''; // Clears the content
const serialOutput = document.getElementById('serialOutput');
const currentTime = new Date().toLocaleTimeString();
const faultCode = document.getElementById('faultCode').innerText; // Get faultCode
const newMessage = `<div> <span class="timestamp">[${currentTime}]</span> ${message}<strong> ${faultCode} </strong></div>`;
serialOutput.innerHTML += newMessage;
// Scroll to the bottom
serialOutput.scrollTop = serialOutput.scrollHeight;
}
//trigger the function every 2 seconds to update the console
setInterval(() => {
addSerialMessage(" ");
}, 2000); // Every 2 seconds, add a new message
</script>
<div class="description">
<div class="admin-controls">
<p>Administrator Controls - Auth Use Only</p>
<div id="unlock-section">
<input type="password" id="admin-password" placeholder="Enter Password">
<button onclick="unlockControls()">Unlock Controls</button>
</div>
<div id="button-section" style="margin-top:20px;">
<div class="button-grid">
<button onclick="syncNTP()" disabled="">Sync with NTP</button>
<button onclick="microUP()" disabled="">microUP</button>
<button onclick="timeAdjustINC()" disabled="">mSADJDAYINC</button>
<button onclick="lockEEPROM()" disabled="">EEPROM</button>
<button onclick="bufferReset()" disabled="">Fault Reset</button>
<button onclick="microDOWN()" disabled="">microDOWN</button>
<button onclick="timeAdjustDEC()" disabled="">mSADJDayDEC</button>
<button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;" ="" onclick="confirmAction('Never Press this Button!', neverPress)" disabled="">
NEVER Press this Button
</button>
</div>
</div>
</div>
<script>
/*
// Original password-protected function (commented out)
const correctPassword = "kev";
function unlockControls() {
const enteredPassword = document.getElementById("admin-password").value;
if (enteredPassword === correctPassword) {
// Enable all buttons after password is correct
const buttons = document.querySelectorAll("#button-section button");
buttons.forEach(button => {
button.disabled = false;
});
document.getElementById("unlock-section").style.display = "none"; // Hide the password input
} else {
alert("Password Incorrect");
}
}
*/
// New function to unlock controls without a password
function unlockControls() {
// Enable all buttons without requiring a password
const buttons = document.querySelectorAll("#button-section button");
buttons.forEach(button => {
button.disabled = false;
});
}
// Automatically unlock controls on page load (optional)
window.onload = unlockControls;
// Confirmation action function
function confirmAction(action, callback) {
if (confirm("Are you sure you want to " + action + "?")) {
callback();
}
}
</script>
</div>
<div class="description">
<h3>ElectroKev Atomic Clock Features</h3>
<div class="mainlinks">
<a href="https://electrokev.com/eacp-code-v49-6/" target="_blank">[24/01/25] ** Source Code View ** v49.6</a><br>
<a href="https://electrokev.com/eacp-code-v50-3/" target="_blank">[28/01/25] ** Source Code View ** v50.3</a><br>
<a href="https://electrokev.com/eacp-code-v50-6/" target="_blank">[29/01/25] ** Source Code View ** v50.6</a>
<span style="color: red;"> * Latest Version *</span>
</div>
<p>The <strong>ElectroKev Atomic Clock</strong> incorporates advanced pulse monitoring and modulation technology, with precise synchronization and real-time correction of the time signal, ensuring microsecond-level accuracy. By tracking pulse drift from an oven-controlled OCXO 10 MHz Crystal, the clock dynamically compensates for drift, maintaining highly accurate timekeeping over extended periods.</p>
<h4>Key Features:</h4>
<ul>
<li><strong>Advanced Pulse Monitoring:</strong> The 10 MHz crystal is scaled by factors of 10 using 6 layers of the 74LS90 integrated circuit, creating a final 10 Hz input. The system continuously monitors and adjusts pulse signals to ensure precise timing.</li>
<li><strong>Real-Time Drift Compensation:</strong> Compensates for long-term drift with microsecond-level accuracy, ensuring stable timekeeping.</li>
<li><strong>User-Controlled Adjustments:</strong> Allows fine-tuning of pulse skipping and drift compensation to achieve optimal accuracy.</li>
<li><strong>Multi-LCD Display System:</strong> Provides detailed data on time, pulse handling, drift calculations, and temperature.</li>
<li><strong>Temperature Monitoring:</strong> Offers real-time temperature data with high precision and alerts for sensor malfunctions.</li>
<li><strong>Output Signal Control:</strong> Generates a modulated output signal with precise timing adjustments for synchronization.</li>
<li><strong>Rotary Encoder Integration:</strong> Enables precise adjustments to pulse modulation intervals with high resolution.</li>
<li><strong>Modular Design:</strong> Easily customizable to integrate additional sensors and outputs, enhancing functionality.</li>
<li><strong>Twin stabilised power supplies:</strong> The system uses twin power supplies, both stabilised and voltage and current regulated for accurate crystal oscillation.</li>
</ul>
<p>
Further Details:
</p>
<p>
The clock is an advanced, pulse-modulated timekeeping system that achieves exceptional precision by synchronizing with an external oven controlled crystal outputting a 10 MHz pulse. This system is designed to manipulate pulses with a remarkable accuracy of up to 1 microsecond per 24 hours, ensuring precise timekeeping over extended periods. It uses a segregated NIST time reference for synchronization, allowing it to maintain accurate time based on trusted atomic standards.
</p>
<p>
For manual adjustments, the clock offers controls to modify the time and date, allowing for fine-tuning through pulse injection or removal. A rotary encoder is used to adjust the daily time alteration rate, with the system automatically calculating the microseconds per pulse to ensure precision. This feature makes it adaptable for environments requiring high-precision time adjustments.
</p>
<p>
The clock is equipped with WiFi connectivity, enabling remote monitoring and control through a web application. This interface provides full access to all major clock functions, allowing users to interact with the system easily via server handlers. Continuous crystal speed monitoring helps identify and debug drift, ensuring the clock remains highly accurate.
</p>
<p>
In terms of fault detection, the clock is designed to identify invalid pulse readings automatically. It removes faulty pulses and logs debug data for analysis, making it resilient to errors. Additionally, the power supply is finely tuned, with regulated power sources dedicated to different components to ensure stable performance.
</p>
<p>
Firmware updates are handled over the air (OTA), ensuring the clock remains up-to-date without requiring manual intervention. The clock operates with a dual-core configuration, separating timekeeping functions from resource-intensive processes to optimize performance. Pulse monitoring inputs are prioritized through high-level processor interrupts for precise counting.
</p>
<p>
The clock also functions as a globally accessible NTP time server on port 123, providing highly accurate time synchronization. A web-based serial console is available for real-time debugging, with self-monitoring and error reporting capabilities to facilitate automatic correction of any issues.
</p>
</div>
<div class="footer">
<a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
© Kevin Davy 2025
</div>
<br><br> Last Code Upload: )rawliteral"
+ String(buildDate) + " at " + String(buildTime) + R"rawliteral(
<p></p>
)rawliteral";
request->send(200, "text/html", html);
}</ticker.h></wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></esp_task_wdt.h></esp_system.h></espasyncwebserver.h></wifi.h></unixtime.h></eeprom.h></liquidcrystal_i2c.h></wire.h></secrets.h>