/****************************************************************************
* 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.
*
***************************************************************************/
const float versionNumber = 49.6;
#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <eeprom.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_bt.h"
#include <preferences.h>
#include <arduinoota.h>
#include <arduinojson.h>
#include <wifiudp.h>
//network stuff
const char *ssid = SSID;
const char *password = PASSWORD;
const char *buildDate = __DATE__;
const char *buildTime = __TIME__;
// Set the static IP details
IPAddress local_ip(192, 168, 1, 208); //local ip
IPAddress gateway(192, 168, 1, 1); // gatewat
IPAddress subnet(255, 255, 255, 0); // subnet
AsyncWebServer server(8080);
Preferences preferences;
// --- NTP Configuration ---
const char *ntpServer = "pool.ntp.org"; // NTP server
const long gmtOffset_sec = 0; // Offset for UTC
const int daylightOffset_sec = 0; // No daylight savings time
WiFiUDP ntpUDP;
const int NTP_PORT =123;
// --- Pin Configurations ---
#define ROTARY_CLK 26 // Rotary encoder clock pin
#define ROTARY_DT 27 // Rotary encoder data pin
#define ROTARY_SW 13 // Rotary encoder switch pin
#define adjustHoursPin 16 // Button pin for adjusting hours
#define adjustMinutesPin 17 // Button pin for adjusting minutes
#define adjustSecondsPin 5 // Button pin for adjusting seconds
#define adjustMicroSecondUp 18 // Button pin for increasing microseconds
#define adjustMicroSecondDown 19 // Button pin for decreasing microseconds
#define syncNTC 23
#define pulsePin 34 // Pulse pin input for counting pulses
#define BUFFER_SIZE 32
// --- Global Objects ---
UnixTime stamp(0);
LiquidCrystal_I2C lcd(0x27, 20, 4);
// --- Time Variables ---
double microSeconds = 0;
double displayMicroSeconds;
double microChange = 0;
float rotaryChangeValue = 0.1; // Rotary encoder adjustment step
float timeAdjust = 0;
double pulseDurationBetweenRisingEdges; //changed from float
double averagePDBRE; //changedfrom float
double totalPDBRE = 100000; //changed from float
double faultyPDBRE;
long espRuntime=0;
long runtimeSeconds = 0;
long runtimeHours=0;
const int crystalCorrection = 0; // Crystal correction factor
int adjustmentsMade;
volatile double currentTime; //changed from long
float xtalFreq;
float totalXtalFreq;
float avgXtalFreq;
int fault = 0;
int firstFault;
int lastUpdate = 0;
// --- Circular Buffer Variables ---
volatile double pulseTimes[BUFFER_SIZE];
volatile int bufferHead = 0;
volatile int bufferTail = 0;
double lastRisingEdgeTime = 0;
int pulseCount = 0;
double globalPulseCount = 0;
// --- Date Setting ---
bool dateSetting = 0;
// --- Time Variables ---
uint32_t nistTime;
uint32_t unix;
int differenceTime;
double lastNistUpdate;
// --- General Variables ---
int visitorsCount;
int freeMemory = 0;
// --- Interrupt Service Routine (ISR) ---
void IRAM_ATTR pulseISR() {
currentTime = esp_timer_get_time();
int nextHead = (bufferHead + 1) % BUFFER_SIZE;
if (nextHead != bufferTail) { // Check if the buffer is not full
pulseTimes[bufferHead] = currentTime;
bufferHead = nextHead;
}
}
// --- Setup Function ---
void setup() {
//Shut down BT
Serial.begin(115200);
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
esp_bt_controller_disable();
esp_bt_controller_deinit();
Serial.println("Bluetooth disabled.");
} else {
Serial.println("Bluetooth was already disabled.");
}
// Setup WiFi connection
setupWifi(); // Starts WiFi and web server (which runs on Core 0)
freeMemory = ESP.getFreeHeap();
Serial.println("Starting OTA...");
ArduinoOTA.begin();
Serial.println("OTA Ready!");
ArduinoOTA.setPassword(nullptr); // Disable password
// --- Rotary Encoder Pins ---
pinMode(ROTARY_CLK, INPUT);
pinMode(ROTARY_DT, INPUT);
pinMode(ROTARY_SW, INPUT_PULLUP);
// --- Pulse Pin Setup ---
pinMode(pulsePin, INPUT);
// --- LCD Initialization ---
lcd.init();
lcd.begin(20, 4);
lcd.backlight();
// --- Button Pins Setup ---
pinMode(adjustHoursPin, INPUT_PULLUP);
pinMode(adjustMinutesPin, INPUT_PULLUP);
pinMode(adjustSecondsPin, INPUT_PULLUP);
pinMode(adjustMicroSecondDown, INPUT_PULLUP);
pinMode(adjustMicroSecondUp, INPUT_PULLUP);
pinMode(ROTARY_SW, INPUT_PULLUP);
pinMode(syncNTC, INPUT_PULLUP);
loadFromEEPROM();
// --- Set Date and Time from Unix Timestamp ---
syncWithNTP();
stamp.getDateTime(unix);
attachInterrupt(digitalPinToInterrupt(pulsePin), pulseISR, FALLING);
Serial.println("Running");
}
// --- Main Loop ---
void loop() {
processPulseBuffer(); // Process pulses from the buffer
handleButtons(); // Check and process button inputs
handleRotaryEncoder(); // Handle rotary encoder inputs
adjustmentRoutine(); // Adjust time if microseconds overflow
ArduinoOTA.handle();
processNTPRequests();
}
void processPulseBuffer() {
static unsigned long lastMillis = millis(); // Track last update time for nistTime
while (bufferTail != bufferHead) {
// Disable interrupts temporarily to process the pulse
noInterrupts();
double pulseTime = pulseTimes[bufferTail];
bufferTail = (bufferTail + 1) % BUFFER_SIZE;
interrupts(); // Re-enable interrupts
pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime);
lastRisingEdgeTime = pulseTime;
// Wait for variables to stabilize
if (runtimeSeconds == 0) {
globalPulseCount = 9;
faultyPDBRE = 0;
totalXtalFreq = 90;
fault = 0;
}
// Process the pulse
if (pulseDurationBetweenRisingEdges >= 99980 && pulseDurationBetweenRisingEdges <= 100020) {
pulseCount++;
} else {
fault++;
if (fault == 1) {
firstFault = runtimeSeconds;
faultyPDBRE = pulseDurationBetweenRisingEdges;
}
}
// Calculate crystal frequency
xtalFreq = ((10000000 * (100000 / pulseDurationBetweenRisingEdges))) / 1000000;
if (runtimeSeconds > 0) {
totalXtalFreq += xtalFreq;
avgXtalFreq = totalXtalFreq / globalPulseCount;
}
//displayVariables();
globalPulseCount++;
// Perform 1-minute tasks
if (runtimeSeconds % 60 == 0) {
freeMemory = ESP.getFreeHeap();
nistUpdate();
lastNistUpdate=nistTime;
}
// Reset after 1 hour
if (runtimeSeconds >= 3600) {
totalPDBRE = 100000;
runtimeSeconds = 0;
runtimeHours++;
averagePDBRE = 0;
xtalFreq = 0;
avgXtalFreq = 0;
totalXtalFreq = 0;
globalPulseCount = 0;
}
// Process accumulated pulses
if (pulseCount >= 10) {
if (runtimeSeconds > 0) {
totalPDBRE += pulseDurationBetweenRisingEdges; // Update total pulse duration
averagePDBRE = totalPDBRE / (runtimeSeconds + 1); // Long-term average pulse duration
}
runtimeSeconds++;
pulseCount = 0;
microSeconds += (microChange * 10); // Add 10x microChange as ten pulses per second
// Adjust display so remaining microseconds are shown in hours between pulse adjustments
if (timeAdjust != 0) {
if (microSeconds < 0) {
displayMicroSeconds = round((100000 + microSeconds) / (10 * abs(microChange) * 3600) * 1000) / 1000;
} else if (microSeconds > 0) {
displayMicroSeconds = round((100000 - microSeconds) / (10 * microChange * 3600) * 1000) / 1000;
}
}
// Update time and display once per successful 10-pulse cycle
updateTime();
updateDisplay();
}
// Increment `nistTime` every 1000 milliseconds based on ESP32 crystal
if (millis() - lastMillis >= 1000) {
nistTime++;
differenceTime = (unix - nistTime);
lastMillis += 1000;
espRuntime++;
if (espRuntime>=3600){
espRuntime=0;
}
}
}
}
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");
}
}
/// --- Handle Rotary Encoder Inputs ---
void handleRotaryEncoder() {
static int lastEncoderState = HIGH;
int currentEncoderState = digitalRead(ROTARY_CLK);
static unsigned long lastEncoderChangeTime = 0;
const unsigned long debounceDelay = 80; // Increased debounce delay
// Check for debounce timing
if ((millis() - lastEncoderChangeTime) > debounceDelay) {
if (currentEncoderState != lastEncoderState) {
// If the direction is different, increment or decrement the value by the desired rotaryChangeValue
if (digitalRead(ROTARY_DT) != currentEncoderState) {
timeAdjust += 0.1; // Adjust by 0.1
} else {
timeAdjust -= 0.1; // Adjust by -0.1
}
// Update the last change time
lastEncoderChangeTime = millis();
}
// Update microseconds adjustment
microChange = ((timeAdjust * 1000000) / 864000); // Calculate microseconds per day
// Correct very small timeAdjust values close to 0
if (abs(timeAdjust) < 0.1) {
timeAdjust = 0.0;
microSeconds = 0;
microChange = 0;
displayMicroSeconds = 0;
}
}
// Update the last encoder state
lastEncoderState = currentEncoderState;
}
// --- Handle Button Inputs ---
void handleButtons() {
static unsigned long lastButtonPressTime = 0;
const unsigned long buttonDebounceInterval = 150;
// Check for debounce interval
if (millis() - lastButtonPressTime > buttonDebounceInterval) {
// Toggle dateSetting if rotary switch is pressed
if (digitalRead(ROTARY_SW) == LOW) {
saveToEEPROM();
dateSetting = !dateSetting; // Toggle the value of dateSetting
}
// Adjust time or date based on dateSetting state
if (!dateSetting) {
// Time adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 3600; // Add one hour
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 60; // Add one minute
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix -= stamp.second; // Reset seconds
pulseCount = 0;
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
pulseCount--; // Decrease pulse count
} else if (digitalRead(adjustMicroSecondUp) == LOW) {
pulseCount++; // Increase pulse count
} else if (digitalRead(syncNTC) == LOW) {
lcd.clear();
lcd.print("WiFi Toggle Button Pressed");
if (WiFi.status() == WL_CONNECTED) {
lcd.print("Turning WiFi OFF...");
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
} else {
lcd.print("Turning WiFi ON...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
}
delay(500);
lcd.clear();
}
} else if (dateSetting) {
// Date adjustment mode
if (digitalRead(adjustHoursPin) == LOW) {
unix += 86400; // Add one day
} else if (digitalRead(adjustMinutesPin) == LOW) {
unix += 2592000; // Add one month (needs refinement for proper month handling)
} else if (digitalRead(adjustSecondsPin) == LOW) {
unix += 31622400; // Add one year
} else if (digitalRead(adjustMicroSecondDown) == LOW) {
unix = 1736521548;
}
}
// Update date and time
stamp.getDateTime(unix);
// Update debounce timer
lastButtonPressTime = millis();
}
}
// --- Update Time ---
void updateTime() {
unix++;
stamp.getDateTime(unix);
}
// --- Update Display ---
void updateDisplay() {
if (dateSetting) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Date Setting Mode");
lcd.setCursor(0, 1);
lcd.print("Adj:");
lcd.print(timeAdjust, 1);
lcd.print(" ");
lcd.print(displayMicroSeconds); // Display positive microseconds
return;
}
// Display full time and date
lcd.setCursor(0, 0);
displayFullTimeDate();
// Display UTC time
lcd.setCursor(0, 1);
lcd.print("UTC: ");
lcd.print(unix); // Display Unix timestamp
lcd.setCursor(0, 2);
lcd.print("NTP: ");
lcd.print(nistTime);
// Display calculated crystal frequency
lcd.setCursor(0, 3);
lcd.print(avgXtalFreq, 6);
lcd.print(" MHz ");
// Display pulse adjustments
lcd.setCursor(14, 3);
lcd.print("PA:");
lcd.print(adjustmentsMade);
}
// --- Adjustment Routine ---
void adjustmentRoutine() {
if (microSeconds >= 100000) {
microSeconds = 0;
pulseCount++;
adjustmentsMade++;
} else if (microSeconds <= -100000) {
microSeconds = 0;
pulseCount--;
adjustmentsMade++;
}
}
void loadFromEEPROM() {
preferences.begin("clock", true); // Open in read-only mode
microChange = preferences.getInt("microChange", 0); // Default value 0
timeAdjust = preferences.getFloat("timeAdjust", 0);
unix = preferences.getLong("unix", 0);
visitorsCount = preferences.getInt("visitorsCount", 0);
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.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();
// Server started message
Serial.println("Server started!");
}
void handleVariables(AsyncWebServerRequest *request) {
// Create a JSON document (this is a fixed-size buffer to hold the JSON)
StaticJsonDocument<1024> doc;
// Fill the JSON document with data
doc["unixTime"] = unix;
doc["nistTime"] = nistTime;
doc["date"] = String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year);
doc["time"] = formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second);
doc["displayMicroSeconds"] = displayMicroSeconds;
doc["microChange"] = microChange;
doc["pulseDurationBetweenRisingEdges"] = pulseDurationBetweenRisingEdges;
doc["timeAdjust"] = timeAdjust;
doc["differenceTime"] = differenceTime;
doc["freeMemory"] = freeMemory;
doc["averagePDBRE"] = String(averagePDBRE, 2);
doc["runtimeSeconds"] = runtimeSeconds;
doc["adjustmentsMade"] = adjustmentsMade;
doc["xtalFreq"] = String(xtalFreq, 6);
doc["avgXtalFreq"] = String(avgXtalFreq, 6);
doc["fault"] = fault;
doc["firstFault"] = firstFault;
doc["globalPulseCount"] = globalPulseCount;
doc["faultyPDBRE"] = faultyPDBRE;
doc["totalPDBRE"] = totalPDBRE;
doc["espRuntime"] = espRuntime;
doc["lastNistUpdate"] = lastNistUpdate;
doc["runtimeHours"] = runtimeHours;
// Serialize the JSON document to a string
String json;
serializeJson(doc, json);
// Send the JSON response
request->send(200, "application/json", json);
}
void nistUpdate() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo2;
if (!getLocalTime(&timeinfo2)) {
return;
}
nistTime = (mktime(&timeinfo2));
}
void syncWithNTP() {
visitorsCount = 0;
// Configure NTP
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Connecting to NTP server...");
// Wait for NTP sync
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 10000)) { // Wait for up to 10 seconds
Serial.println("Failed to get time from NTP server.");
lcd.clear();
lcd.print("NTP Sync Failed!");
return;
}
// Convert to Unix timestamp and update time
unix = (mktime(&timeinfo))+1;
nistTime = (mktime(&timeinfo));
microSeconds = 0; // Reset microsecond adjustments
pulseCount = 0;
stamp.getDateTime(unix);
// Debug Output
Serial.println("NTP Sync Successful!");
Serial.print("Unix Time: ");
Serial.println(unix);
// Update LCD
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("NTP Sync OK!");
lcd.setCursor(0, 1);
lcd.print("Time Updated.");
}
void routeHandlers() {
// Define route handlers
server.on("/", HTTP_GET, handleRoot);
server.on("/variables", HTTP_GET, handleVariables);
// Synchronize time with NTP
server.on("/syncNTP", HTTP_GET, [](AsyncWebServerRequest *request) {
syncWithNTP();
request->send(200, "text/plain", "NTP Sync Completed");
});
// Increase pulse count
server.on("/microUP", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount++;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroUp");
request->send(200, "text/plain", "Pulse Count Increased");
});
// Decrease pulse count
server.on("/microDOWN", HTTP_GET, [](AsyncWebServerRequest *request) {
pulseCount--;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MicroDown");
request->send(200, "text/plain", "Pulse Count Decreased");
});
// Increase time adjust
server.on("/timeAdjustINC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust += 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust INC");
request->send(200, "text/plain", "Time Adjust Increased");
});
// Decrease time adjust
server.on("/timeAdjustDEC", HTTP_GET, [](AsyncWebServerRequest *request) {
timeAdjust -= 0.1;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("timeAdjust DEC");
request->send(200, "text/plain", "Time Adjust Decreased");
});
// Get version number
server.on("/getVersion", HTTP_GET, [](AsyncWebServerRequest *request) {
String version = String(versionNumber, 1);
request->send(200, "application/json", "{\"version\":\"" + version + "\"}");
});
// Lock data to EEPROM
server.on("/lockEEPROM", HTTP_GET, [](AsyncWebServerRequest *request) {
saveToEEPROM();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("EEPROM Saved");
request->send(200, "text/plain", "Saved to EEPROM");
});
// Reset buffer
server.on("/bufferReset", HTTP_GET, [](AsyncWebServerRequest *request) {
fault = 0;
firstFault = 0;
firstFault = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Fault Reset");
request->send(200, "text/plain", "Buffer Reset");
});
// Restart the ESP32
server.on("/neverPress", HTTP_GET, [](AsyncWebServerRequest *request) {
ESP.restart();
request->send(200, "text/plain", "Never Press");
});
}
void handleRoot(AsyncWebServerRequest *request) {
// Increment visitor count on each webpage load
visitorsCount++;
String html = R"rawliteral(
<meta charset="UTF-8">
<title>ElectroKev Atomic Clock EACP</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1f1f1f, #333);
color: #e0e0e0;
font-family: 'Arial', sans-serif;
}
.time {
font-size: 4rem;
font-weight: bold;
text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
margin-bottom: 20px;
color: #00ff00;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #222;
color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 600px;
margin: 20px auto;
}
.variables {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
width: 100%;
max-width: 800px;
text-align: left;
padding: 10px;
}
.variable {
font-size: 1rem;
padding: 12px 20px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
border: 0px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.header {
font-size: 2.5rem;
font-weight: bold;
color: #00ff00;
text-align: center;
}
.header2 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
text-align: center;
margin-top: -10px;
}
.subheader {
font-size: 1.2rem;
color: #bbb;
text-align: center;
margin-top: 10px;
font-style: italic;
}
.version-container {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
font-size: 1.1rem;
}
.version {
color: #fff;
font-weight: bold;
}
.last-updated {
color: #00ff00;
font-weight: normal;
}
.ntp {
color: orange;
font-weight: normal;
}
.footer {
margin-top: 30px;
font-size: 1.2rem;
text-align: center;
color: #ccc;
}
.footer a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.mainlinks a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.description {
font-size: 1.2rem;
text-align: left;
color: #e0e0e0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
margin-top: 30px;
width: 100%;
max-width: 800px;
}
.description h3 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
}
/* General button style */
button {
font-family: 'Arial', sans-serif;
font-size: 1.2rem;
padding: 10px 20px;
border: none;
border-radius: 8px;
background-color: #4CAF50; /* Green background */
color: white;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease; /* Smooth transition */
}
/* Hover effect */
button:hover {
background-color: #45a049; /* Darker green */
transform: scale(1.05); /* Slightly enlarges on hover */
}
/* Active button (when clicked) */
button:active {
background-color: #3e8e41; /* Even darker green */
transform: scale(0.98); /* Slightly shrinks on click */
}
/* Disabled button */
button:disabled {
background-color: #b0b0b0; /* Gray background */
color: #d3d3d3; /* Light gray text */
cursor: not-allowed;
transform: none; /* No scaling on disabled buttons */
}
/* Focus outline */
button:focus {
outline: none; /* Remove default focus outline */
box-shadow: 0 0 5px rgba(72, 133, 255, 0.5); /* Add custom focus outline */
}
/* Optional: Add some padding to buttons in a container */
.button-container {
display: flex;
gap: 20px;
justify-content: center;
margin-top: 20px;
}
@media (max-width: 1024px) {
.time { font-size: 6rem; }
.variable { font-size: 2rem; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
@media (max-width: 768px) {
.time { font-size: 8rem; }
.variable { font-size: 2.5rem; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 2rem; }
.description { font-size: 1rem; }
}
.admin-controls {
text-align: center;
margin: 20px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Adjust button width */
gap: 10px; /* Space between buttons */
justify-items: center;
margin-top: 20px;
}
.button-grid button {
padding: 10px 20px;
font-size: 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
}
.button-grid button:hover {
background-color: #45a049;
}
</style>
<script>
function syncNTP() {
fetch('/syncNTP')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function microDOWN() {
fetch('/microDOWN')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function microUP() {
fetch('/microUP')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustINC() {
fetch('/timeAdjustINC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustDEC() {
fetch('/timeAdjustDEC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function bufferReset() {
fetch('/bufferReset')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function neverPress() {
fetch('/neverPress')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function lockEEPROM() {
fetch('/lockEEPROM')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function updateVariables() {
fetch('/variables')
.then(response => response.json())
.then(data => {
document.getElementById('unixTime').innerText = data.unixTime;
document.getElementById('nistTime').innerText = data.nistTime;
document.getElementById('date').innerText = data.date;
document.getElementById('time').innerText = data.time;
document.getElementById('displayMicroSeconds').innerText = data.displayMicroSeconds;
document.getElementById('microChange').innerText = data.microChange;
document.getElementById('pulseDurationBetweenRisingEdges').innerText = data.pulseDurationBetweenRisingEdges;
document.getElementById('timeAdjust').innerText = data.timeAdjust;
document.getElementById('differenceTime').innerText = data.differenceTime;
document.getElementById('freeMemory').innerText = data.freeMemory;
document.getElementById('averagePDBRE').innerText = data.averagePDBRE;
document.getElementById('runtimeSeconds').innerText = data.runtimeSeconds;
document.getElementById('totalPDBRE').innerText = data.totalPDBRE;
document.getElementById('adjustmentsMade').innerText = data.adjustmentsMade;
document.getElementById('xtalFreq').innerText = data.xtalFreq;
document.getElementById('avgXtalFreq').innerText = data.avgXtalFreq;
document.getElementById('fault').innerText = data.fault;
document.getElementById('firstFault').innerText = data.firstFault;
document.getElementById('globalPulseCount').innerText = data.globalPulseCount;
document.getElementById('faultyPDBRE').innerText = data.faultyPDBRE;
document.getElementById('espRuntime').innerText = data.espRuntime;
document.getElementById('lastNistUpdate').innerText = data.lastNistUpdate;
document.getElementById('runtimeHours').innerText = data.runtimeHours;
});
}
setInterval(updateVariables, 250);
</script>
<div class="container">
<div class="header">ElectroKev Atomic Clock Project
<span class="header2">(EACP)</span></div>
<div class="subheader">Developed by Kevin Davy | January 2025</div>
<div class="version-container">
<span id="version" class="version">Loading version...</span>
<span id="lastUpdated" class="last-updated">Last Code Upload: )rawliteral"
+ String(buildDate) + " at " + String(buildTime) + R"rawliteral(</span>
</div>
<div class="version-container">
<span id="version" class="version">NTP Server: </span>
<span id="ntp" class="ntp">clock.electrokev.com Port 123</span> <span style="color: green;">STATUS: OK</span>
</div>
<script>
function updateVersion() {
fetch('/getVersion')
.then(response => response.json())
.then(data => {
document.getElementById('version').innerText = "Code Version " + data.version;
});
}
updateVersion();
</script>
</div>
<div class="variables">
<div class="variable" style="color: orangered;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
<div class="variable" style="color: turquoise;"><strong>EACP UNIX Time:<br></strong> <span id="unixTime">-</span></div>
<div class="variable" style="color: yellow;"><strong>NIST UNIX Time:<br></strong> <span id="nistTime">-</span></div>
<div class="variable" style="color: orange;"><strong>Last NIST Update:<br></strong> <span id="lastNistUpdate">-</span></div>
<div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
<div class="variable"><strong>Global PC:<br></strong> <span id="globalPulseCount">-</span></div>
<div class="variable"><strong>μs/Pulse+-:<br></strong> <span id="microChange">-</span></div>
<div class="variable"><strong>PulseAdj/Next:<br></strong> <span id="adjustmentsMade">-</span> / <span id="displayMicroSeconds">-</span> Hrs</div>
<div class="variable"><strong>PDBRE:<br></strong> <span id="pulseDurationBetweenRisingEdges">-</span> μS</div>
<div class="variable"><strong>Avg PDBRE:<br></strong> <span id="averagePDBRE">-</span> μs</div>
<div class="variable"><strong>Adjust:<br></strong> <span id="timeAdjust">-</span> s/Day</div>
<div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> KB</div>
<div class="variable"><strong>RunTime (Sec/Hrs)<br></strong> <span id="runtimeSeconds">-</span> / <span id="runtimeSeconds">-</span></div>
<div class="variable"><strong>ESP RunTime<br></strong> <span id="espRuntime">-</span> / 3600</div>
<div class="variable"><strong>Total PDBRE<br></strong> <span id="totalPDBRE">-</span> μs</div>
<div class="variable"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
<div class="variable"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
<div class="variable"><strong>Accuracy within:<br></strong> <span id="differenceTime">-</span> seconds</div>
<div class="variable" style="color: red;"><strong>Invalid Pulses:<br></strong> <span id="fault">-</span></div>
<div class="variable" style="color: red;"><strong>First Fault at:<br></strong> <span id="firstFault">-</span> Sec <span id="faultyPDBRE">-</span></div>
</div>
<div class="description">
<div class="admin-controls">
<p>Administrator Controls - Auth Use Only</p>
<!-- Unlock Section -->
<div id="unlock-section">
<input type="password" id="admin-password" placeholder="Enter Password">
<button onclick="unlockControls()">Unlock Controls</button>
</div>
<!-- Buttons Section (Initially hidden) -->
<div id="button-section" style="display: none;">
<div class="button-grid">
<button onclick="syncNTP()">Sync with NTP</button>
<button onclick="microUP()">microUP</button>
<button onclick="microDOWN()">microDOWN</button>
<button onclick="timeAdjustINC()">mSADJDAYINC</button>
<button onclick="timeAdjustDEC()">mSADJDayDEC</button>
<button onclick="lockEEPROM()">EEPROM</button>
<button onclick="bufferReset()">Fault Reset</button>
<button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;" ="" onclick="confirmAction('Never Press this Button!', neverPress)">
NEVER Press this Button
</button>
</div>
</div>
</div>
<script>
function confirmAction(action, callback) {
if (confirm("Are you sure you want to " + action + "?")) {
callback();
}
}
</script>
<script>
// password for control panel
const correctPassword = "kev";
function unlockControls() {
const enteredPassword = document.getElementById("admin-password").value;
if (enteredPassword === correctPassword) {
document.getElementById("button-section").style.display = "block"; // Show the buttons
document.getElementById("unlock-section").style.display = "none"; // Hide the password input
} else {
alert("Password Incorrect");
}
}
</script>
</div>
<div class="description">
<h3>ElectroKev Atomic Clock Features</h3>
<div class="mainlinks">
<a href="https://electrokev.com/eacp-code-v49-0/" target="_blank">** Source Code View ** v49.0</a><br>
<a href="https://electrokev.com/eacp-code-v49-5/" target="_blank">** Source Code View ** v49.5</a><br>
<a href="https://electrokev.com/eacp-code-v49-6/" target="_blank">** Source Code View ** v49.6</a>
<span style="color: red;"> ** Latest Stable Version **</span>
</div>
<p>The <strong>ElectroKev Atomic Clock</strong> incorporates advanced pulse monitoring and modulation technology, with precise synchronization and real-time correction of the time signal, ensuring microsecond-level accuracy. By tracking pulse drift from an oven-controlled OCXO 10 MHz Crystal, the clock dynamically compensates for drift, maintaining highly accurate timekeeping over extended periods.</p>
<h4>Key Features:</h4>
<ul>
<li><strong>Advanced Pulse Monitoring:</strong> The 10 MHz crystal is scaled by factors of 10 using 6 layers of the 74LS90 integrated circuit, creating a final 10 Hz input. The system continuously monitors and adjusts pulse signals to ensure precise timing.</li>
<li><strong>Real-Time Drift Compensation:</strong> Compensates for long-term drift with microsecond-level accuracy, ensuring stable timekeeping.</li>
<li><strong>User-Controlled Adjustments:</strong> Allows fine-tuning of pulse skipping and drift compensation to achieve optimal accuracy.</li>
<li><strong>Multi-LCD Display System:</strong> Provides detailed data on time, pulse handling, drift calculations, and temperature.</li>
<li><strong>Temperature Monitoring:</strong> Offers real-time temperature data with high precision and alerts for sensor malfunctions.</li>
<li><strong>Output Signal Control:</strong> Generates a modulated output signal with precise timing adjustments for synchronization.</li>
<li><strong>Rotary Encoder Integration:</strong> Enables precise adjustments to pulse modulation intervals with high resolution.</li>
<li><strong>Modular Design:</strong> Easily customizable to integrate additional sensors and outputs, enhancing functionality.</li>
<li><strong>Twin stabilised power supplies:</strong> The system uses twin power supplies, both stabilised and voltage and current regulated for accurate crystal oscillation.</li>
</ul>
</div>
<div class="footer">
<a href="https://electrokev.com" target="_blank">ElectroKev.com</a><br>
© Kevin Davy 2025
<br><br> Last Code Upload: )rawliteral"
+ String(buildDate) + " at " + String(buildTime) + R"rawliteral(
</div>
<p></p>
)rawliteral";
request->send(200, "text/html", html);
}</wifiudp.h></arduinojson.h></arduinoota.h></preferences.h></espasyncwebserver.h></wifi.h></unixtime.h></eeprom.h></liquidcrystal_i2c.h></wire.h></secrets.h>