/***************************************************************
* Project: Time-Sync Pulse Tracking Atomic Clock
* Description:
* Oven controlled crystal clock that syncs to an external 10Mhz pulse
* and allows fine microsecond μS adjustment via rotary encoder.
* UTC compatible time coding
* Pulse manipulation for fine accuracy
* WIFI Server enabled for webpage administration
* OTA Update Compatible
* Copyright (C) 2025 Kevin Davy
* All rights reserved.
***************************************************************/
const float versionNumber = 48.3;
#include <secrets.h>
#include <wire.h>
#include <liquidcrystal_i2c.h>
#include <eeprom.h>
#include <unixtime.h>
#include <wifi.h>
#include <espasyncwebserver.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_bt.h"
#include <preferences.h>
#include <arduinoota.h>
//network stuff
const char *ssid = SSID;
const char *password = PASSWORD;
// 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
// --- 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 16
// --- 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;
float pulseDurationBetweenRisingEdges;
float averagePDBRE;
float totalPDBRE = 100000;
long runtimeSeconds = 0;
const int crystalCorrection = 0; // Crystal correction factor
int adjustmentsMade;
unsigned long currentTime;
float xtalFreq;
float totalXtalFreq;
float avgXtalFreq;
// --- Circular Buffer Variables ---
volatile unsigned long pulseTimes[BUFFER_SIZE];
volatile int bufferHead = 0;
volatile int bufferTail = 0;
float lastRisingEdgeTime = 0;
int pulseCount = 0;
int globalPulseCount=0;
// --- Date Setting ---
bool dateSetting = 0;
// --- Time Variables ---
uint32_t nistTime;
uint32_t unix;
int differenceTime;
// --- General Variables ---
int visitorsCount;
int freeMemory = 0;
// --- Interrupt Service Routine (ISR) ---
void IRAM_ATTR pulseISR() {
currentTime = esp_timer_get_time();
int nextHead = (bufferHead + 1) % BUFFER_SIZE;
if (nextHead != bufferTail) { // Check if the buffer is not full
pulseTimes[bufferHead] = currentTime;
globalPulseCount++;
pulseCount++;
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, RISING);
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();
}
void loadFromEEPROM() {
preferences.begin("clock", true); // Open in read-only mode
microChange = preferences.getInt("microChange", 0); // Default value 0
timeAdjust = preferences.getFloat("timeAdjust", 0);
unix = preferences.getLong("unix", 0);
visitorsCount = preferences.getInt("visitorsCount", 0);
preferences.end();
}
// --- Process Pulse Buffer ---
void processPulseBuffer() {
while (bufferTail != bufferHead) {
noInterrupts(); // Disable interrupts temporarily to process the pulse
float pulseTime = pulseTimes[bufferTail];
bufferTail = (bufferTail + 1) % BUFFER_SIZE;
interrupts(); // Re-enable interrupts
//wait for variables to stabilise;
if (runtimeSeconds==1){
globalPulseCount=11;
totalXtalFreq=100;
}
// Process the pulse
pulseDurationBetweenRisingEdges = (pulseTime - lastRisingEdgeTime);
xtalFreq = (10000000 * (100000 / pulseDurationBetweenRisingEdges)) / 1000000;
totalXtalFreq += xtalFreq;
if (runtimeSeconds > 0) {
avgXtalFreq = totalXtalFreq / globalPulseCount;
}
Serial.print("pulseCount: ");
Serial.print(pulseCount);
Serial.print(" PDBRE: ");
Serial.print(pulseDurationBetweenRisingEdges);
Serial.print(" micros: ");
Serial.print(micros());
Serial.print(" BT: ");
Serial.print(bufferTail);
Serial.print(" RuntimeSec: ");
Serial.print(runtimeSeconds);
Serial.print(" totalXtalFreq: ");
Serial.print(totalXtalFreq);
Serial.print(" globalPulseCount: ");
Serial.print(globalPulseCount);
Serial.print(" avgXtalFreq: ");
Serial.println(avgXtalFreq,6);
lastRisingEdgeTime = pulseTime;
// Reset after 1 hour
if (runtimeSeconds > 3600) {
totalPDBRE = 100000;
runtimeSeconds = 0;
averagePDBRE = 0;
freeMemory = ESP.getFreeHeap();
xtalFreq=0;
avgXtalFreq=0;
totalXtalFreq=0;
globalPulseCount=0;
}
if (pulseCount >= 10) {
if (runtimeSeconds > 0) {
totalPDBRE = totalPDBRE + pulseDurationBetweenRisingEdges; // Update total pulse duration
//calculate a longer term average pulse duration
averagePDBRE = totalPDBRE / (runtimeSeconds + 1);
}
runtimeSeconds++;
pulseCount = 0;
microSeconds += (microChange * 10); //add 10 x microchange as ten pulses per second
//Adjust display so microSeconds counts down rather than up
if (timeAdjust != 0) {
if (microSeconds <= 0) {
displayMicroSeconds = round((100000 + microSeconds) * 100) / 100.0;
} else {
displayMicroSeconds = round((100000 - microSeconds) * 100) / 100.0;
}
}
// Update time, display, and external clock reference
updateTime();
updateDisplay();
nistUpdate();
}
}
}
/// --- 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.001) {
timeAdjust = 0.0;
microSeconds = 0;
microChange = 0;
displayMicroSeconds = 100000;
}
}
// 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);
lcd.setCursor(0, 2);
displayFullTimeDate();
// Update debounce timer
lastButtonPressTime = millis();
}
}
// --- Update Time ---
void updateTime() {
unix++;
stamp.getDateTime(unix);
}
// --- Update Display ---
void updateDisplay() {
if (dateSetting) {
lcd.clear();
lcd.print("Date Setting Mode");
lcd.setCursor(0, 2);
displayFullTimeDate();
return;
}
// Display microseconds adjustment
lcd.setCursor(0, 0);
lcd.print("Adj:");
lcd.print(timeAdjust, 1);
lcd.print(" ");
lcd.print(displayMicroSeconds); // Display positive microseconds
// Display UTC time
lcd.setCursor(0, 1);
lcd.print("UTC: ");
lcd.print(unix); // Display Unix timestamp
// Display full time and date
lcd.setCursor(0, 2);
displayFullTimeDate();
// Display pulse duration between rising edges
lcd.setCursor(0, 3);
/*
lcd.print(pulseDurationBetweenRisingEdges, 0); // Print pulse duration
lcd.print(" PD "); // Pulse Duration Between Rising Edges
*/
lcd.print(avgXtalFreq,6);
lcd.print(" MHz");
// Display visitor count
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 saveToEEPROM() {
preferences.begin("clock", false); // Open a namespace (false = read/write mode)
preferences.putInt("microChange", microChange);
preferences.putFloat("timeAdjust", timeAdjust);
preferences.putLong("unix", unix);
preferences.putInt("visitorsCount", visitorsCount);
preferences.end(); // Close the Preferences storage
}
// --- Format Time ---
String formatTime(int timeValue) {
return (timeValue < 10) ? "0" + String(timeValue) : String(timeValue);
}
void displayFullTimeDate() {
// Get the current time and date from the Unix timestamp
int currentYear = (stamp.year); // Current year
int currentMonth = (stamp.month); // Current month (1-12)
int currentDay = (stamp.day); // Current day
int currentHour = (stamp.hour); // Current hour
int currentMinute = (stamp.minute); // Current minute
int currentSecond = (stamp.second); // Current second
// Display time and date on the LCD
lcd.print(currentDay);
lcd.print("/");
lcd.print(currentMonth);
lcd.print("/");
lcd.print(currentYear);
// lcd.setCursor(0, 1);
lcd.print(" ");
lcd.print(formatTime(currentHour));
lcd.print(":");
lcd.print(formatTime(currentMinute));
lcd.print(":");
lcd.print(formatTime(currentSecond));
}
// --- Display Variables in Serial Monitor ---
void displayVariables() {
static unsigned long lastDisplayTime = 0;
const unsigned long displayInterval = 10000;
if (millis() - lastDisplayTime >= displayInterval) {
Serial.println("----- Variable State -----");
Serial.print("Unix Time: ");
Serial.println(unix);
Serial.print("Microseconds: ");
Serial.println(microSeconds, 6);
Serial.print("Micro Change: ");
Serial.println(microChange, 6);
Serial.print("Pulse Count: ");
Serial.println(pulseCount);
Serial.print("Pulse Duration Between Rising Edges: ");
Serial.println(pulseDurationBetweenRisingEdges, 6);
Serial.print("Buffer Head: ");
Serial.println(bufferHead);
Serial.print("Buffer Tail: ");
Serial.println(bufferTail);
Serial.print("Date Setting: ");
Serial.println(dateSetting ? "ON" : "OFF");
Serial.print("Rotary Change Value: ");
Serial.println(rotaryChangeValue, 6);
Serial.print("Time Adjust Value: ");
Serial.println(timeAdjust);
// Print current date and time
Serial.print("Formatted Date and Time: ");
Serial.print(stamp.day);
Serial.print("/");
Serial.print(stamp.month);
Serial.print("/");
Serial.print(stamp.year);
Serial.print(" ");
Serial.print(formatTime(stamp.hour));
Serial.print(":");
Serial.print(formatTime(stamp.minute));
Serial.print(":");
Serial.println(formatTime(stamp.second));
Serial.println("--------------------------");
lastDisplayTime = millis();
}
}
void setupWifi() {
// Begin WiFi connection
WiFi.begin(ssid, password);
// Wait for the WiFi connection
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
// Display WiFi connection details
Serial.println("Connected to WiFi");
Serial.println(WiFi.localIP());
// Set up route handlers
routeHandlers();
// Start the server
server.begin();
// Server started message
Serial.println("Server started!");
}
void handleVariables(AsyncWebServerRequest *request) {
// Prepare the JSON string
String json = "{";
json += "\"unixTime\":" + String(unix) + ",";
json += "\"nistTime\":" + String(nistTime) + ",";
json += "\"date\":\"" + String(stamp.day) + "/" + String(stamp.month) + "/" + String(stamp.year) + "\",";
json += "\"time\":\"" + formatTime(stamp.hour) + ":" + formatTime(stamp.minute) + ":" + formatTime(stamp.second) + "\",";
json += "\"displayMicroSeconds\":" + String(displayMicroSeconds, 6) + ",";
json += "\"microChange\":" + String(microChange, 6) + ",";
json += "\"pulseDurationBetweenRisingEdges\":" + String(pulseDurationBetweenRisingEdges) + ",";
json += "\"timeAdjust\":" + String(timeAdjust) + ",";
json += "\"differenceTime\":" + String(differenceTime) + ",";
json += "\"visitorsCount\":" + String(visitorsCount) + ",";
json += "\"freeMemory\":" + String(freeMemory) + ",";
json += "\"averagePDBRE\":" + String(averagePDBRE) + ",";
json += "\"runtimeSeconds\":" + String(runtimeSeconds) + ",";
json += "\"adjustmentsMade\":" + String(adjustmentsMade) + ",";
json += "\"xtalFreq\":" + String(xtalFreq, 6) + ",";
json += "\"avgXtalFreq\":" + String(avgXtalFreq, 6) + ",";
json += "\"totalPDBRE\":" + String(totalPDBRE);
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)) + 1;
differenceTime = (unix - nistTime);
}
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;
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) {
bufferHead = 0;
bufferTail = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Buffer Reset");
request->send(200, "text/plain", "Buffer Reset");
});
// Restart the ESP32
server.on("/neverPress", HTTP_GET, [](AsyncWebServerRequest *request) {
ESP.restart();
request->send(200, "text/plain", "Never Press");
});
}
void handleRoot(AsyncWebServerRequest *request) {
// Increment visitor count on each webpage load
visitorsCount++;
String html = R"rawliteral(
<meta charset="UTF-8">
<title>ElectroKev Atomic Clock EACP</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1f1f1f, #333);
color: #e0e0e0;
font-family: 'Arial', sans-serif;
}
.time {
font-size: 4rem;
font-weight: bold;
text-shadow: 0 0 5px #0f0, 0 0 10px #00ff00;
margin-bottom: 20px;
color: #00ff00;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #222;
color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
max-width: 600px;
margin: 20px auto;
}
.variables {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
width: 100%;
max-width: 800px;
text-align: left;
padding: 10px;
}
.variable {
font-size: 1rem;
padding: 12px 20px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
border: 0px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}
.header {
font-size: 2.5rem;
font-weight: bold;
color: #00ff00;
text-align: center;
}
.header2 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
text-align: center;
margin-top: -10px;
}
.subheader {
font-size: 1.2rem;
color: #bbb;
text-align: center;
margin-top: 10px;
font-style: italic;
}
.version-container {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 10px;
font-size: 1.1rem;
}
.version {
color: #fff;
font-weight: bold;
}
.last-updated {
color: #00ff00;
font-weight: normal;
}
.footer {
margin-top: 30px;
font-size: 1.2rem;
text-align: center;
color: #ccc;
}
.footer a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.mainlinks a {
color: #00ff00;
text-decoration: none;
font-weight: bold;
}
.footer a:hover {
text-decoration: underline;
}
.description {
font-size: 1.2rem;
text-align: left;
color: #e0e0e0;
padding: 20px;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
margin-top: 30px;
width: 100%;
max-width: 800px;
}
.description h3 {
font-size: 1.5rem;
font-weight: bold;
color: #ffdd00;
}
/* General button style */
button {
font-family: 'Arial', sans-serif;
font-size: 1.2rem;
padding: 10px 20px;
border: none;
border-radius: 8px;
background-color: #4CAF50; /* Green background */
color: white;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease; /* Smooth transition */
}
/* Hover effect */
button:hover {
background-color: #45a049; /* Darker green */
transform: scale(1.05); /* Slightly enlarges on hover */
}
/* Active button (when clicked) */
button:active {
background-color: #3e8e41; /* Even darker green */
transform: scale(0.98); /* Slightly shrinks on click */
}
/* Disabled button */
button:disabled {
background-color: #b0b0b0; /* Gray background */
color: #d3d3d3; /* Light gray text */
cursor: not-allowed;
transform: none; /* No scaling on disabled buttons */
}
/* Focus outline */
button:focus {
outline: none; /* Remove default focus outline */
box-shadow: 0 0 5px rgba(72, 133, 255, 0.5); /* Add custom focus outline */
}
/* Optional: Add some padding to buttons in a container */
.button-container {
display: flex;
gap: 20px;
justify-content: center;
margin-top: 20px;
}
@media (max-width: 1024px) {
.time { font-size: 6rem; }
.variable { font-size: 2rem; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 1.5rem; }
.description { font-size: 1rem; }
}
@media (max-width: 768px) {
.time { font-size: 8rem; }
.variable { font-size: 2.5rem; }
.header, .header2 { font-size: 3rem; }
.footer { font-size: 2rem; }
.description { font-size: 1rem; }
}
.admin-controls {
text-align: center;
margin: 20px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Adjust button width */
gap: 10px; /* Space between buttons */
justify-items: center;
margin-top: 20px;
}
.button-grid button {
padding: 10px 20px;
font-size: 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
}
.button-grid button:hover {
background-color: #45a049;
}
</style>
<script>
function syncNTP() {
fetch('/syncNTP')
.then(response => response.text())
.then(data => console.log(data)) // Log the response
.catch(error => console.error('Error:', error));
}
function microDOWN() {
fetch('/microDOWN')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function microUP() {
fetch('/microUP')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustINC() {
fetch('/timeAdjustINC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function timeAdjustDEC() {
fetch('/timeAdjustDEC')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function bufferReset() {
fetch('/bufferReset')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function neverPress() {
fetch('/neverPress')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function lockEEPROM() {
fetch('/lockEEPROM')
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
}
function updateVariables() {
fetch('/variables')
.then(response => response.json())
.then(data => {
document.getElementById('unixTime').innerText = data.unixTime;
document.getElementById('nistTime').innerText = data.nistTime;
document.getElementById('date').innerText = data.date;
document.getElementById('time').innerText = data.time;
document.getElementById('displayMicroSeconds').innerText = data.displayMicroSeconds;
document.getElementById('microChange').innerText = data.microChange;
document.getElementById('pulseDurationBetweenRisingEdges').innerText = data.pulseDurationBetweenRisingEdges;
document.getElementById('timeAdjust').innerText = data.timeAdjust;
document.getElementById('differenceTime').innerText = data.differenceTime;
document.getElementById('visitorsCount').innerText = data.visitorsCount;
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;
});
}
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 Updated 20th January 2025.</span>
</div>
<script>
function updateVersion() {
fetch('/getVersion')
.then(response => response.json())
.then(data => {
document.getElementById('version').innerText = "Code Version " + data.version;
});
}
updateVersion();
</script>
</div>
<div class="variables">
<div class="variable" style="color: orangered;"><strong>EACP Time:<br></strong> <span id="time">-</span></div>
<div class="variable" style="color: turquoise;"><strong>EACP UNIX Time:<br></strong> <span id="unixTime">-</span></div>
<div class="variable" style="color: yellow;"><strong>NIST UNIX Time:<br></strong> <span id="nistTime">-</span></div>
<div class="variable"><strong>Accuracy within:<br></strong> <span id="differenceTime">-</span> seconds</div>
<div class="variable"><strong>Date:<br></strong> <span id="date">-</span></div>
<div class="variable"><strong>PulseAdj/Next:<br></strong> <span id="adjustmentsMade">-</span> / <span id="displayMicroSeconds">-</span> μs</div>
<div class="variable"><strong>μs/Pulse+-:<br></strong> <span id="microChange">-</span></div>
<div class="variable"><strong>PDBRE:<br></strong> <span id="pulseDurationBetweenRisingEdges">-</span> μS</div>
<div class="variable"><strong>Adjust:</strong> <span id="timeAdjust">-</span> s/Day</div>
<div class="variable"><strong>Users:</strong> <span id="visitorsCount">-</span></div>
<div class="variable"><strong>Free Memory:<br></strong> <span id="freeMemory">-</span> KB</div>
<div class="variable"><strong>Avg PDRBE:<br></strong> <span id="averagePDBRE">-</span> Hz</div>
<div class="variable"><strong>RunTime<br></strong> <span id="runtimeSeconds">-</span>/3600</div>
<div class="variable"><strong>Total PDBRE<br></strong> <span id="totalPDBRE">-</span> μs</div>
<div class="variable"><strong>xTal Freq<br></strong> <span id="xtalFreq">-</span> MHz</div>
<div class="variable"><strong>Avg xTal Freq<br></strong> <span id="avgXtalFreq">-</span> MHz</div>
</div>
<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()">bufferReset</button>
<button style="background-color: red; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;" ="" onclick="confirmAction('Never Press this Button!', neverPress)">
NEVER Press this Button
</button>
</div>
</div>
</div>
<script>
function confirmAction(action, callback) {
if (confirm("Are you sure you want to " + action + "?")) {
callback();
}
}
</script>
<script>
// password for control panel
const correctPassword = "kev";
function unlockControls() {
const enteredPassword = document.getElementById("admin-password").value;
if (enteredPassword === correctPassword) {
document.getElementById("button-section").style.display = "block"; // Show the buttons
document.getElementById("unlock-section").style.display = "none"; // Hide the password input
} else {
alert("Password Incorrect");
}
}
</script>
</div>
<div class="description">
<h3>ElectroKev Atomic Clock Features</h3>
<div class="mainlinks">
<a href="https://electrokev.com/eacp-code" target="_blank">** Source Code ** View Source Code v47.6</a><br>
<a href="https://electrokev.com/eacp-code-v48-3/" target="_blank">** Source Code ** View Source Code v48.3</a><br>
</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>
</div>
<p></p>
)rawliteral";
request->send(200, "text/html", html);
}</arduinoota.h></preferences.h></time.h></espasyncwebserver.h></wifi.h></unixtime.h></eeprom.h></liquidcrystal_i2c.h></wire.h></secrets.h>