A very quick note on my Fluke PM6680 Universal Counter. How to add a £5 OCXO to compete with some of the best 10mhz references out there. It also includes an Arduino Wemos D1 Pro Mini to monitor the temperature and the code to display that data on a webpage along with a RSSI. T the time of writing there is no DS18B20 attached, hence -127c . Firstly, the project was merely to add an OCXO to my PM6680B but believe the pinouts for the PM6681 and others are similar “however and a word of caution”, some Universal counters from Fluke utilise a 24v OCXO but this can be passed into a buck converter (has the potential to introduce switching noise) or a decent high current regulator to bring it down to 12vdc. The board is very, very basic and merely a piggy back board from a cheap OCXO. The clever part is really the code and the ability to plot and adjust the Vref. As the OCXO which comprises of a OSC5Ab02 v2 (important as the v1 was totally pathetic). The v2 has v scoring around the regulator for better heat control but the main difference is the stability. I don’t own the v1 but many have said it’s excellent aside from some early issues “regulator heating and apparently better run at 9vdc rather than full on 12vdc”.
Please use the calculators page to find the date to seconds converter and calculate the OCXO age to seconds. I decided not to add this feature to the Web GUI and rather enter the age through serial monitor. BEWARE though!! any future updates to the code with require you to note down the crystal ovens age and then enter that age in serial monitor: Example, the OCXO is 1 year old, you would take the number from the calculator and enter > setage 31,
Finally the code between the Astrix
***************************************************************************************************************
#include <Wire.h>
#include <Adafruit_MCP4725.h>
#include <EEPROM.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// ———————————————————————————————————————————————
// CONFIG from here onwards – please don’t mess with it unless you understand what its doing
// ———————————————————————————————————————————————
#define ONE_WIRE_BUS D4
#define EEPROM_SIZE 2048 // Increased EEPROM size to accommodate more calibration points
#define SSID_ADDR 0 // 32 bytes
#define PASS_ADDR 32 // 64 bytes
#define TRIM_ADDR 96 // uint16_t
#define OCXO_ACCUM_UPTIME_ADDR 160 // uint64_t for accumulated uptime
#define AUTOTRIM_MODE_ADDR 168 // bool for auto trim mode
#define NBANKS 3
#define MAX_CAL_POINTS 10 // Reverted to 10 calibration points as requested
#define CAL_COUNT_ADDR(b) (120 + (b)*((1 + MAX_CAL_POINTS*sizeof(CalPoint))))
#define CAL_ENTRY_ADDR(b) (CAL_COUNT_ADDR(b) + 1)
#define DAC_RES 4096
#define DAC_VCC 5.0
#define MAX_DAC_VOLTAGE 1.0 // ±1 V swing
#define MAX_DAC_COUNT ((uint16_t)((MAX_DAC_VOLTAGE/DAC_VCC)*(DAC_RES-1)))
#define TEMP_INTERVAL 5000 // ms (for sensor reads and EEPROM save)
#define EEPROM_SAVE_INTERVAL 60000 // ms (save accumulated uptime every minute)
const char* CONFIG_AP_SSID = “G5STO_Config”;
const char* CONFIG_AP_PWD = “config1234”;
// ———————————————————————————————————————————————
// GLOBAL Values – Again leave well alone
// ———————————————————————————————————————————————
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
Adafruit_MCP4725 dac;
ESP8266WebServer server(80); // Global declaration for server
char storedSSID[32];
char storedPASS[64];
uint16_t trimVal = MAX_DAC_COUNT/2; // Global declaration for trimVal
float currentTemp = NAN; // Global declaration for currentTemp
int currentRSSI = -127; // Global declaration for currentRSSI
unsigned long lastTempMs = 0;
uint8_t currentBank = 0; // Global declaration for currentBank
uint64_t ocxoAccumulatedUptime = 0; // Stored in EEPROM for OCXO age
unsigned long lastEepromSaveMs = 0; // To control EEPROM write frequency
bool autoTrimEnabled = true; // New: Controls whether auto-trimming is active
// mDNS event
WiFiEventHandler onGotIP;
// Calibration point struct + table
// Added age_ms to CalPoint to store OCXO age at calibration time
struct CalPoint { uint16_t t10; uint16_t trim; uint64_t age_ms; };
CalPoint calPts[MAX_CAL_POINTS];
uint8_t calCount = 0;
// ———————————————————————————————————————————————
// PROTOTYPE Code (Declares all functions before their definitions if they are called out of order)
// ———————————————————————————————————————————————
void loadCalibration(uint8_t bank);
void saveCalibrationPoint(uint8_t bank, uint16_t t10, uint16_t trim, uint64_t age_ms); // Updated prototype
void resetCalibration(uint8_t bank);
uint16_t lookupTrim(uint8_t bank, float tempC);
void onStationGotIP(const WiFiEventStationModeGotIP& evt);
void handleData();
void handleSet();
void handleCal();
void handleReset();
void handleBank();
void handleMap();
void handleRoot(); // Prototype for the main UI handler
void handleConfig();
void handleMode(); // New: Handles auto trim mode changes
// Removed handleResetOcxoAge() prototype as it’s now handled via Serial
// ———————————————————————————————————————————————
// Wi-Fi “got IP” callback
// ———————————————————————————————————————————————
void onStationGotIP(const WiFiEventStationModeGotIP& evt) {
Serial.printf(“\n✅ STA IP: %s\n”, evt.ip.toString().c_str());
if (MDNS.begin(“G5STO”)) {
Serial.println(“🔗 http://G5STO.local”);
}
}
// ———————————————————————————————————————————————
// /data → JSON { temp, ssid, rssi, uptime_seconds, ocxo_accum_uptime, auto_trim_enabled }
// ———————————————————————————————————————————————
void handleData() {
String j = “{“;
j += “\”temp\”:” + String(currentTemp,1) + “,”;
j += “\”ssid\”:\”” + WiFi.SSID() + “\”,”;
j += “\”rssi\”:” + String(currentRSSI);
j += “,\”uptime_seconds\”:” + String(millis() / 1000); // ESP’s current uptime
j += “,\”ocxo_accum_uptime\”:” + String((unsigned long)(ocxoAccumulatedUptime / 1000)); // Send in seconds
j += String(“,\”auto_trim_enabled\”:”) + (autoTrimEnabled ? “true” : “false”); // New: Send auto trim mode
j += “}”;
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “application/json; charset=utf-8”, j); // Added charset to Content-Type
}
// ———————————————————————————————————————————————
// /set?val=NNN → clamp & set DAC + EEPROM
// ———————————————————————————————————————————————
void handleSet() {
Serial.println(“handleSet received request.”); // Diagnostic print
if (server.hasArg(“val”)) {
int v = server.arg(“val”).toInt();
Serial.printf(“Received value from web: %d\n”, v); // Diagnostic print
v = constrain(v, 0, MAX_DAC_COUNT);
trimVal = v;
Serial.printf(“DEBUG: Before DAC setVoltage (manual) – trimVal: %u\n”, trimVal); // ADDED DEBUG PRINT
dac.setVoltage(trimVal, false); // Set DAC voltage
Serial.printf(“DAC set to value: %u (%.3f V)\n”, trimVal, (float)trimVal * DAC_VCC / (DAC_RES – 1)); // Diagnostic print
EEPROM.put(TRIM_ADDR, trimVal);
if (EEPROM.commit()) {
Serial.println(“EEPROM commit successful.”); // Diagnostic print
} else {
Serial.println(“EEPROM commit failed!”); // Diagnostic print
}
} else {
Serial.println(“No ‘val’ argument found in /set request.”); // Diagnostic print
}
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(204); // Send No Content response
}
// ———————————————————————————————————————————————
// /cal → save current temp→trim to this bank (now also saves OCXO age)
// ———————————————————————————————————————————————
void handleCal() {
uint16_t t10 = round(currentTemp * 10);
saveCalibrationPoint(currentBank, t10, trimVal, ocxoAccumulatedUptime); // Pass accumulated uptime
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “text/plain”, “OK”);
}
// ———————————————————————————————————————————————
// /reset → clear this bank’s map
// ———————————————————————————————————————————————
void handleReset() {
resetCalibration(currentBank);
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “text/plain”, “OK”);
}
// ———————————————————————————————————————————————
// /bank?b=N → switch bank
// ———————————————————————————————————————————————
void handleBank() {
if (server.hasArg(“b”)) {
int b = server.arg(“b”).toInt();
if (b >= 0 && b < NBANKS) {
currentBank = b;
loadCalibration(currentBank);
}
}
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(204);
}
// ———————————————————————————————————————————————
// /map → return JSON array of {t,trim,age_ms}
// ———————————————————————————————————————————————
void handleMap() {
String j = “[“;
for (uint8_t i = 0; i < calCount; i++) {
j += “{\”t\”:” + String(calPts[i].t10/10.0,1) // Server sends already scaled temp
+ “,\”trim\”:” + String(calPts[i].trim)
+ “,\”age_ms\”:” + String((unsigned long)(calPts[i].age_ms / 1000)) + “}”; // Send age in seconds
if (i < calCount – 1) j += “,”;
}
j += “]”;
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “application/json; charset=utf-8”, j); // Added charset to Content-Type
}
// ———————————————————————————————————————————————
// /mode?m=auto|learn → set auto trim mode
// ———————————————————————————————————————————————
void handleMode() {
if (server.hasArg(“m”)) {
String mode = server.arg(“m”);
if (mode == “auto”) {
autoTrimEnabled = true;
Serial.println(“Auto Trim Mode: ON”);
} else if (mode == “learn”) {
autoTrimEnabled = false;
Serial.println(“Auto Trim Mode: OFF (Learn Mode)”);
}
EEPROM.put(AUTOTRIM_MODE_ADDR, autoTrimEnabled);
if (EEPROM.commit()) {
Serial.println(“Auto Trim Mode committed to EEPROM.”);
} else {
Serial.println(“Failed to commit Auto Trim Mode to EEPROM!”);
}
Serial.printf(“DEBUG: handleMode – autoTrimEnabled set to %s\n”, autoTrimEnabled ? “true” : “false”);
}
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(204); // No Content
}
// ———————————————————————————————————————————————
// handleConfig(): Handles the Wi-Fi configuration page
// ———————————————————————————————————————————————
void handleConfig() {
if (server.method() == HTTP_POST) {
// Save new SSID and password
if (server.hasArg(“ssid”) && server.hasArg(“password”)) {
String newSsid = server.arg(“ssid”);
String newPass = server.arg(“password”);
newSsid.toCharArray(storedSSID, sizeof(storedSSID));
newPass.toCharArray(storedPASS, sizeof(storedPASS));
EEPROM.put(SSID_ADDR, storedSSID);
EEPROM.put(PASS_ADDR, storedPASS);
EEPROM.commit();
String response = “<html><body><h1>Credentials Saved!</h1>”;
response += “<p>Attempting to connect to: ” + newSsid + “</p>”;
response += “<p>Restarting ESP in 5 seconds…</p>”;
response += “<script>setTimeout(function(){ window.location.href = ‘/’; }, 5000);</script>”;
response += “</body></html>”;
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “text/html”, response);
delay(100);
ESP.restart(); // Restart to connect with new credentials
}
} else {
// Display config form
String html = “<html><body>”;
html += “<h1>Wi-Fi Configuration</h1>”;
html += “<form method=’POST’ action=’/config’>”;
html += “SSID: <input type=’text’ name=’ssid’ value='” + String(storedSSID) + “‘><br>”;
html += “Password: <input type=’password’ name=’password’><br>”;
html += “<input type=’submit’ value=’Save and Connect’>”;
html += “</form>”;
html += “</body></html>”;
server.sendHeader(“X-Content-Type-Options”, “nosniff”); // Added security header
server.sendHeader(“Cache-Control”, “no-cache, no-store, must-revalidate”); // Added caching header
server.send(200, “text/html”, html);
}
}
// ———————————————————————————————————————————————
// C++ helper function for temperature status (used in initial HTML render)
// ———————————————————————————————————————————————
String getTempStatusCpp(float celsius) {
if (isnan(celsius)) { return “N/A”; }
if (celsius < 25) { return “Warming up”; }
else if (celsius >= 25 && celsius <= 50) { return “Last phase”; }
else { return “Warmed up”; }
}
// ———————————————————————————————————————————————
// C++ helper function for formatting uptime (used in initial HTML render)
// ———————————————————————————————————————————————
String formatTimeCpp(uint64_t totalSeconds) {
uint64_t years = totalSeconds / (365 * 24 * 3600);
totalSeconds %= (365 * 24 * 3600);
uint64_t days = totalSeconds / (24 * 3600);
totalSeconds %= (24 * 3600);
uint64_t hours = totalSeconds / 3600;
totalSeconds %= 3600;
uint64_t minutes = totalSeconds / 60;
char buffer[100]; // Sufficient buffer size
String result = “”;
if (years > 0) {
sprintf(buffer, “%llu years “, years);
result += buffer;
}
if (days > 0) {
sprintf(buffer, “%llu days “, days);
result += buffer;
}
sprintf(buffer, “%02llu hours %02llu minutes”, hours, minutes); // Use %02llu for padding
result += buffer;
result.trim(); // Trim in place
return result;
}
// ———————————————————————————————————————————————
// handleRoot(): Serves the main web page (dynamically generated)
// ———————————————————————————————————————————————
void handleRoot() {
// Calculate initial values for display
uint16_t currentTrimVal = trimVal; // Use a local copy for display
float currentVolts = (float)currentTrimVal * DAC_VCC / (DAC_RES – 1);
// Initial uptime for display
unsigned long initialUptimeSeconds = millis() / 1000;
int h = initialUptimeSeconds / 3600;
int m = (initialUptimeSeconds % 3600) / 60;
int s = initialUptimeSeconds % 60;
// Debug print for the value used to generate HTML
Serial.printf(“DEBUG: handleRoot – currentTrimVal for HTML: %u\n”, currentTrimVal);
String html = String(“<!DOCTYPE html>”);
html += String(“<html lang=’en’>”);
html += String(“<head>”);
html += String(“<meta charset=’utf-8′>”);
html += String(“<meta name=’viewport’ content=’width=device-width, initial-scale=1.0′>”);
html += String(“<title>G5STO – OCXO Monitor & Trim</title>”);
html += String(“<script src=’https://cdn.jsdelivr.net/npm/chart.js’></script>”); //Powerful JS scripting for charts – Not my work
html += String(“<style>”);
html += String(“body { font-family: ‘Inter’, sans-serif; text-align: center; margin: 0; padding: 1em; background-color: #f0f2f5; color: #333; box-sizing: border-box; }”);
html += String(“h1, h2, h3, h4 { color: #2c3e50; margin-top: 0; }”);
html += String(“main { display: flex; flex-wrap: wrap; justify-content: center; gap: 1.5em; max-width: 1200px; margin: 1em auto; }”);
html += String(“.card { background-color: #ffffff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); padding: 1.5em; flex: 1 1 calc(33% – 3em); min-width: 280px; box-sizing: border-box; transition: transform 0.2s ease-in-out; }”);
html += String(“.card:hover { transform: translateY(-5px); }”);
html += String(“.display-value { font-size: 2.2em; font-weight: 700; color: #3498db; margin: 0.5em 0; }”);
html += String(“.display-label { font-size: 0.9em; color: #7f8c8d; margin-bottom: 0.5em; }”);
html += String(“meter { width: 100%; height: 25px; margin: 0.5em 0; border-radius: 5px; overflow: hidden; -webkit-appearance: none; appearance: none; }”);
html += String(“meter::-webkit-meter-bar { background: #ecf0f1; border-radius: 5px; }”);
html += String(“meter::-webkit-meter-optimum-value { background: #2ecc71; border-radius: 5px; }”);
html += String(“meter::-webkit-meter-suboptimum-value { background: #f39c12; border-radius: 5px; }”);
html += String(“meter::-webkit-meter-unsatisfied-value { background: #e74c3c; border-radius: 5px; }”);
html += String(“button, select { background-color: #3498db; color: white; border: none; padding: 0.7em 1.2em; margin: 0.3em; border-radius: 8px; cursor: pointer; font-size: 0.9em; font-weight: 600; transition: background-color 0.3s ease, transform 0.1s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }”);
html += String(“button:hover, select:hover { background-color: #2980b9; transform: translateY(-1px); }”);
html += String(“button:active { transform: translateY(0); box-shadow: none; }”);
html += String(“.button-group { margin-top: 1em; }”);
html += String(“.primary-btn { background-color: #27ae60; }”);
html += String(“.primary-btn:hover { background-color: #229a56; }”);
html += String(“.secondary-btn { background-color: #e74c3c; }”);
html += String(“.secondary-btn:hover { background-color: #c0392b; }”);
html += String(“input[type=’range’] { width: 90%; margin: 1em 0; -webkit-appearance: none; appearance: none; height: 8px; background: #bdc3c7; outline: none; border-radius: 5px; transition: opacity .2s; }”);
html += String(“input[type=’range’]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #3498db; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }”);
html += String(“input[type=’range’]::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: #3498db; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }”);
html += String(“.slider-value-display { font-size: 1.1em; font-weight: bold; color: #3498db; margin-top: 0.5em; }”);
html += String(“pre { background-color: #ecf0f1; border: 1px solid #dde1e6; border-radius: 8px; padding: 1em; max-height: 200px; overflow: auto; text-align: left; white-space: pre-wrap; word-wrap: break-word; margin-top: 1em; font-family: ‘Courier New’, monospace; font-size: 0.9em; }”);
html += String(“#calChart, #ageDriftChart { width: 100%; max-height: 300px; margin-top: 1em; background-color: #ffffff; border-radius: 8px; padding: 1em; box-shadow: 0 2px 6px rgba(0,0,0,0.05); }”);
html += String(“#feedbackMessage { margin-top: 1em; padding: 0.8em; border-radius: 8px; font-weight: bold; opacity: 0; transition: opacity 0.5s ease-in-out; }”);
html += String(“#feedbackMessage.success { background-color: #d4edda; color: #155724; opacity: 1; }”);
html += String(“#feedbackMessage.error { background-color: #f8d7da; color: #721c24; opacity: 1; }”);
html += String(“#feedbackMessage.info { background-color: #cfe2ff; color: #052c65; opacity: 1; }”);
html += String(“footer { margin-top: 2em; padding: 1em; color: #7f8c8d; font-size: 0.85em; }”);
html += String(“.auto-trim-toggle-btn { background-color: #27ae60; width: auto; min-width: 120px; padding: 0.7em 1.2em; margin-top: 1em; }”);
html += String(“.auto-trim-toggle-btn.off { background-color: #e74c3c; }”);
html += String(“.auto-trim-toggle-btn:hover { transform: translateY(-1px); }”);
html += String(“.auto-trim-toggle-btn.off:hover { background-color: #c0392b; }”);
html += String(“@media (max-width: 992px) { .card { flex: 1 1 calc(50% – 3em); } }”);
html += String(“@media (max-width: 600px) { .card { flex: 1 1 100%; } }”);
html += String(“</style>”);
html += String(“</head>”);
html += String(“<body>”);
html += String(“<header><h1>G5STO – OCXO Monitor & Trim</h1></header>”);
html += String(“<main>”);
html += String(“<section class=’card’>”);
html += String(“<h2>System Status</h2>”);
html += String(“<div class=’display-label’>Uptime:</div>”);
html += String(“<p class=’display-value’ id=’uptime’>”) + String(h) + “:” + String(m/10) + String(m%10) + “:” + String(s/10) + String(s%10) + String(“</p>”);
html += String(“<label for=’bank’ class=’display-label’>Calibration Bank:</label>”);
html += String(“<select id=’bank’ onchange=’chgBank(this.value)’>”);
for (int i = 0; i < NBANKS; i++) {
html += String(“<option value='”) + String(i) + String(“‘”);
if (i == currentBank) {
html += String(” selected”);
}
html += String(“>Bank “) + String(i + 1) + String(“</option>”);
}
html += String(“</select>”);
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>Temperature (DS18B20)</h2>”);
html += String(“<p class=’display-value’><span id=’temp’>”) + String(currentTemp, 1) + String(“</span> <button id=’toggleTempUnit’ onclick=’toggleTempUnit()’>F</button></p>”); // Initial temp
html += String(“<p class=’display-label’><span id=’tempStatus’>”) + getTempStatusCpp(currentTemp) + String(“</span></p>”); // Initial temp status
html += String(“<meter id=’tempG’ min=’-40′ max=’125′ low=’0′ high=’50’ optimum=’25’ value='”) + String(currentTemp, 1) + String(“‘></meter>”); // Initial temp gauge
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>Wi-Fi Signal</h2>”);
html += String(“<p class=’display-label’>SSID: <span id=’ssid’>”) + WiFi.SSID() + String(“</span></p>”); // Initial SSID
html += String(“<p class=’display-value’><span id=’rssiTxt’>”) + String(currentRSSI) + String(“</span> dBm</p>”); // Initial RSSI text
html += String(“<meter id=’rssiG’ min=’-100′ max=’0′ low=’-80′ high=’-60′ optimum=’-40′ value='”) + String(currentRSSI) + String(“‘></meter>”); // Initial RSSI gauge
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>OCXO Trim (±”) + String(MAX_DAC_VOLTAGE, 1) + String(” V)</h2>”);
html += String(“<input type=’range’ id=’coarse’ min=’0′ max='”) + String(MAX_DAC_COUNT) + String(“‘ value='”) + String(currentTrimVal) + String(“‘ oninput=’updateSliderDisplay(this.value)’ onchange=’setDAC(this.value)’ title=’OCXO Trim Value’>”);
// Corrected: Initial voltage display without ‘ V’ inside the span. JS will add it.
html += String(“<p class=’slider-value-display’><span id=’dacValueDisplay’>”) + String(currentVolts, 3) + String(“</span></p>”); // Removed ‘ V’ from here
html += String(“<div class=’button-group’>”);
html += String(“<button onclick=’nudge(-10)’>-10</button>”);
html += String(“<button onclick=’nudge(-1)’>-1</button>”);
html += String(“<button onclick=’nudge(1)’>+1</button>”);
html += String(“<button onclick=’nudge(10)’>+10</button>”);
html += String(“</div>”);
html += String(“<div class=’button-group’>”);
html += String(“<button class=’primary-btn’ onclick=’saveCal()’>Save Cal for Current Temp</button>”);
html += String(“<button class=’secondary-btn’ onclick=’resetCal()’>Reset Cal Map</button>”);
html += String(“</div>”);
html += String(“<p id=’feedbackMessage’></p>”);
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>OCXO Age</h2>”);
html += String(“<p class=’display-value’ id=’ocxoAge’>”) + formatTimeCpp(ocxoAccumulatedUptime / 1000) + String(“</p>”); // Initial OCXO age
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>Auto Trim Control</h2>”);
html += String(“<button id=’autoTrimToggle’ class=’auto-trim-toggle-btn”) + (autoTrimEnabled ? “” : ” off”) + String(“‘ onclick=’toggleAutoTrimMode()’>Auto Trim: “) + (autoTrimEnabled ? “ON” : “OFF (Learn Mode)”) + String(“</button>”); // Initial auto trim state
html += String(“<p class=’display-label’>When OFF, manual trim adjustments are not overwritten by temperature.</p>”);
html += String(“</section>”);
html += String(“<section class=’card’>”);
html += String(“<h2>Calibration Data Table</h2>”);
html += String(“<button onclick=’viewTable()’>View Table</button>”);
html += String(“<pre id=’map’></pre>”);
html += String(“</section>”);
html += String(“<section class=’card’ style=’flex: 1 1 100%;’>”);
html += String(“<h2>Calibration Plot (Temp vs. Voltage)</h2>”);
html += String(“<button onclick=’viewPlot()’>View Plot</button>”);
html += String(“<canvas id=’calChart’></canvas>”);
html += String(“</section>”);
html += String(“<section class=’card’ style=’flex: 1 1 100%;’>”);
html += String(“<h2>Voltage Drift over OCXO Age</h2>”);
html += String(“<button onclick=’viewAgeDriftPlot()’>View Age Drift Plot</button>”);
html += String(“<canvas id=’ageDriftChart’></canvas>”);
html += String(“</section>”);
html += String(“</main>”);
html += String(“<footer><p>© 2025 G5STO OCXO Monitor. All rights reserved.</p></footer>”);
html += String(“<script>”);
html += String(“const MAX_DAC_COUNT = “) + String(MAX_DAC_COUNT) + String(“;”);
html += String(“const DAC_RES = “) + String(DAC_RES) + String(“;”);
html += String(“const DAC_VCC = “) + String(DAC_VCC, 3) + String(“;”);
html += String(“const MAX_DAC_VOLTAGE = “) + String(MAX_DAC_VOLTAGE, 3) + String(“;”);
html += String(“const TEMP_INTERVAL = “) + String(TEMP_INTERVAL) + String(“;”);
html += String(“const NBANKS = “) + String((int)NBANKS) + String(“;”);
html += String(“const CELSIUS_WARMING_UP_MAX = 25;”);
html += String(“const CELSIUS_LAST_PHASE_MAX = 50;”);
html += String(“let calChart;”);
html += String(“let ageDriftChart;”);
html += String(“let dacSetTimeout;”);
html += String(“let isFahrenheit = localStorage.getItem(‘isFahrenheit’) === ‘true’;”);
html += String(“let currentUptimeSeconds = “) + String(initialUptimeSeconds) + String(“;”);
html += String(“let ocxoAccumulatedUptimeSeconds = “) + String((unsigned long)(ocxoAccumulatedUptime / 1000)) + String(“;”);
html += String(“let isAutoTrimEnabled = “) + (autoTrimEnabled ? String(“true”) : String(“false”)) + String(“;”);
html += String(“function showFeedback(message, type = ‘info’, duration = 3000) {“);
html += String(” const feedback = document.getElementById(‘feedbackMessage’);”);
html += String(” feedback.innerText = message;”);
html += String(” feedback.className = String(‘feedback-msg ‘) + type;”);
html += String(” clearTimeout(feedback.timer);”);
html += String(” feedback.timer = setTimeout(() => { feedback.className = String(‘feedback-msg’); feedback.innerText = ”; }, duration);”);
html += String(“}”);
html += String(“function updateSliderDisplay(value) {“);
html += String(” const volts = (value / (DAC_RES – 1)) * DAC_VCC;”);
html += String(” document.getElementById(‘dacValueDisplay’).innerText = volts.toFixed(3) + ‘ V’;”);
html += String(“}”);
html += String(“function convertCelsiusToFahrenheit(celsius) {“);
html += String(” return (celsius * 9 / 5) + 32;”);
html += String(“}”);
html += String(“function getTempStatus(celsius) {“);
html += String(” if (celsius < CELSIUS_WARMING_UP_MAX) { return ‘Warming up’; }”);
html += String(” else if (celsius >= CELSIUS_WARMING_UP_MAX && celsius <= CELSIUS_LAST_PHASE_MAX) { return ‘Last phase’; }”);
html += String(” else { return ‘Warmed up’; }”);
html += String(“}”);
html += String(“function formatTime(totalSeconds) {“);
html += String(” const years = Math.floor(totalSeconds / (365 * 24 * 3600));”);
html += String(” totalSeconds %= (365 * 24 * 3600);”);
html += String(” const days = Math.floor(totalSeconds / (24 * 3600));”);
html += String(” totalSeconds %= (24 * 3600);”);
html += String(” const hours = Math.floor(totalSeconds / 3600);”);
html += String(” totalSeconds %= 3600;”);
html += String(” const minutes = Math.floor(totalSeconds / 60);”);
html += String(” let result = ”;”);
html += String(” if (years > 0) result += String(years) + ‘ years ‘;”);
html += String(” if (days > 0) result += String(days) + ‘ days ‘;”);
html += String(” result += String(hours).padStart(2, ‘0’) + ‘ hours ‘ + String(minutes).padStart(2, ‘0’) + ‘ minutes’;”);
html += String(” return result.trim();”);
html += String(“}”);
html += String(“async function upd(){“);
html += String(” console.log(‘Fetching data from /data…’);”); // Added debug log
html += String(” try {“);
html += String(” let r = await fetch(‘/data’);”);
html += String(” if (!r.ok) {“);
html += String(” const errorText = await r.text();”); // Get response text for more info
html += String(” throw new Error(String(‘Network response for /data was not ok: ‘) + r.status + ‘ – ‘ + errorText);”);
html += String(” }”);
html += String(” const rawResponse = await r.text();”); // Get raw response text
html += String(” console.log(‘Raw /data response:’, rawResponse);”); // Log raw response
html += String(” let d = JSON.parse(rawResponse);”); // Parse JSON from raw text
html += String(” console.log(‘Parsed /data:’, d);”); // Log parsed JSON
html += String(” let displayTemp = d.temp;”);
html += String(” let tempUnit = ‘°C’;”);
html += String(” if (isFahrenheit) { displayTemp = convertCelsiusToFahrenheit(d.temp); tempUnit = ‘°F’; }”);
html += String(” document.getElementById(‘temp’).innerText = String(displayTemp.toFixed(1)) + ‘ ‘ + tempUnit;”);
html += String(” document.getElementById(‘tempG’).value = d.temp;”);
html += String(” document.getElementById(‘tempStatus’).innerText = getTempStatus(d.temp);”);
html += String(” document.getElementById(‘ssid’).innerText = d.ssid;”);
html += String(” document.getElementById(‘rssiG’).value = d.rssi;”);
html += String(” document.getElementById(‘rssiTxt’).innerText = String(d.rssi) + ‘ dBm’;”);
html += String(” currentUptimeSeconds = d.uptime_seconds;”);
html += String(” ocxoAccumulatedUptimeSeconds = d.ocxo_accum_uptime;”);
html += String(” isAutoTrimEnabled = d.auto_trim_enabled;”);
html += String(” updateAutoTrimToggleUI();”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error fetching or parsing data:’, error);”); // Updated error message
html += String(” showFeedback(String(‘Failed to update data: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“let lastClientUptimeUpdate = Date.now();”);
html += String(“function updateUptimeAndAgeDisplay() {“);
html += String(” const now = Date.now();”);
html += String(” const elapsed = (now – lastClientUptimeUpdate) / 1000;”);
html += String(” lastClientUptimeUpdate = now;”);
html += String(” currentUptimeSeconds += elapsed;”);
html += String(” const h = Math.floor(currentUptimeSeconds / 3600);”);
html += String(” const m = Math.floor((currentUptimeSeconds % 3600) / 60);”);
html += String(” const s = Math.floor(currentUptimeSeconds % 60);”);
html += String(” document.getElementById(‘uptime’).innerText = String(h) + ‘:’ + String(m).padStart(2, ‘0’) + ‘:’ + String(s).padStart(2, ‘0’);”);
html += String(” ocxoAccumulatedUptimeSeconds += elapsed;”);
html += String(” document.getElementById(‘ocxoAge’).innerText = formatTime(ocxoAccumulatedUptimeSeconds);”);
html += String(“}”);
html += String(“function toggleTempUnit() {“);
html += String(” isFahrenheit = !isFahrenheit;”);
html += String(” localStorage.setItem(‘isFahrenheit’, isFahrenheit);”);
html += String(” document.getElementById(‘toggleTempUnit’).innerText = isFahrenheit ? String(‘C’) : String(‘F’);”);
html += String(” upd();”);
html += String(“}”);
html += String(“function setDAC(v){“);
html += String(” console.log(‘setDAC called with value:’, v);”); // ADDED CLIENT-SIDE DEBUG
html += String(” clearTimeout(dacSetTimeout);”);
html += String(” dacSetTimeout = setTimeout(async () => {“);
html += String(” try {“);
html += String(” console.log(‘Attempting to fetch /set?val=’ + v);”); // ADDED CLIENT-SIDE DEBUG
html += String(” const response = await fetch(String(‘/set?val=’) + v);”);
html += String(” if (!response.ok) throw new Error(String(‘Network response for /set was not ok: ‘) + response.status);”);
html += String(” document.getElementById(‘coarse’).value = v;”);
html += String(” showFeedback(‘DAC value set successfully!’, ‘success’);”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error setting DAC:’, error);”);
html += String(” showFeedback(String(‘Failed to set DAC: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(” }, 150);”);
html += String(“}”);
html += String(“function nudge(d){“);
html += String(” console.log(‘nudge called with delta:’, d);”); // ADDED CLIENT-SIDE DEBUG
html += String(” let c = document.getElementById(‘coarse’), nv = +c.value + d;”);
html += String(” nv = Math.max(0, Math.min(nv, MAX_DAC_COUNT));”);
html += String(” updateSliderDisplay(nv);”);
html += String(” setDAC(nv);”);
html += String(“}”);
html += String(“async function saveCal(){“);
html += String(” try {“);
html += String(” const response = await fetch(‘/cal’);”);
html += String(” if (!response.ok) throw new Error(String(‘Network response for /cal was not ok: ‘) + response.status);”);
html += String(” showFeedback(‘Calibration point saved!’, ‘success’);”);
html += String(” viewTable();”);
html += String(” viewPlot();”);
html += String(” viewAgeDriftPlot();”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error saving calibration:’, error);”);
html += String(” showFeedback(String(‘Failed to save calibration: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“async function resetCal(){“);
html += String(” if (confirm(‘Are you sure you want to clear the calibration map for this bank?’)) {“);
html += String(” try {“);
html += String(” const response = await fetch(‘/reset’);”);
html += String(” if (!response.ok) throw new Error(String(‘Network response for /reset was not ok: ‘) + response.status);”);
html += String(” document.getElementById(‘map’).textContent = ‘No calibration points saved for this bank.’;”);
html += String(” calChart.data.labels = [];”);
html += String(” calChart.data.datasets[0].data = [];”);
html += String(” calChart.update();”);
html += String(” ageDriftChart.data.labels = [];”);
html += String(” ageDriftChart.data.datasets[0].data = [];”);
html += String(” ageDriftChart.update();”);
html += String(” showFeedback(‘Calibration map reset!’, ‘success’);”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error resetting calibration:’, error);”);
html += String(” showFeedback(String(‘Failed to reset calibration: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(” }”);
html += String(“}”);
html += String(“async function viewTable(){“);
html += String(” try {“);
html += String(” let r = await fetch(‘/map’);”);
html += String(” if (!r.ok) throw new Error(String(‘Network response for /map was not ok: ‘) + r.status);”);
html += String(” let m = await r.json();”);
html += String(” let s = ”;”);
html += String(” if (m.length === 0) { s = ‘No calibration points saved for this bank.’; }”);
html += String(” else {“);
html += String(” s += String(‘Temp (°C) | Trim | Age (s)\\n’);”);
html += String(” s += String(‘————————-\\n’);”);
html += String(” m.forEach(p => s += String(p.t.toFixed(1)) + ‘ °C | ‘ + String(p.trim) + ‘ | ‘ + String(p.age_ms) + ‘\\n’);”);
html += String(” }”);
html += String(” document.getElementById(‘map’).textContent = s;”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error fetching map data:’, error);”);
html += String(” showFeedback(String(‘Failed to load calibration table: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“async function viewPlot(){“);
html += String(” try {“);
html += String(” let r = await fetch(‘/map’);”);
html += String(” if (!r.ok) throw new Error(String(‘Network response for /map was not ok: ‘) + r.status);”);
html += String(” let m = await r.json();”);
html += String(” const temps = m.map(p => parseFloat(p.t));”);
html += String(” const volts = m.map(p => (p.trim / (DAC_RES – 1)) * DAC_VCC);”);
html += String(” calChart.data.labels = temps;”);
html += String(” calChart.data.datasets[0].data = volts;”);
html += String(” calChart.update();”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error fetching map data for Temp vs. Voltage plot:’, error);”);
html += String(” showFeedback(String(‘Failed to load calibration plot: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“async function viewAgeDriftPlot(){“);
html += String(” try {“);
html += String(” let r = await fetch(‘/map’);”);
html += String(” if (!r.ok) throw new Error(String(‘Network response for /map was not ok: ‘) + r.status);”);
html += String(” let m = await r.json();”);
html += String(” m.sort((a, b) => a.age_ms – b.age_ms);”);
html += String(” const ages = m.map(p => formatTime(p.age_ms));”);
html += String(” const volts = m.map(p => (p.trim / (DAC_RES – 1)) * DAC_VCC);”);
html += String(” ageDriftChart.data.labels = ages;”);
html += String(” ageDriftChart.data.datasets[0].data = volts;”);
html += String(” ageDriftChart.update();”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error fetching map data for Age Drift plot:’, error);”);
html += String(” showFeedback(String(‘Failed to load age drift plot: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“async function chgBank(b){“);
html += String(” try {“);
html += String(” const response = await fetch(String(‘/bank?b=’) + b);”);
html += String(” if (!response.ok) throw new Error(String(‘Network response for /bank was not ok: ‘) + response.status);”);
html += String(” showFeedback(String(‘Switched to Bank ‘) + (parseInt(b) + 1) + ‘!’, ‘success’);”);
html += String(” location.reload();”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error changing bank:’, error);”);
html += String(” showFeedback(String(‘Failed to switch bank: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“async function toggleAutoTrimMode() {“);
html += String(” const newState = !isAutoTrimEnabled;”);
html += String(” const modeParam = newState ? String(‘auto’) : String(‘learn’);”);
html += String(” try {“);
html += String(” const response = await fetch(String(‘/mode?m=’) + modeParam);”);
html += String(” if (!response.ok) throw new Error(String(‘Network response for /mode was not ok: ‘) + response.status);”);
html += String(” isAutoTrimEnabled = newState;”);
html += String(” updateAutoTrimToggleUI();”);
html += String(” showFeedback(String(‘Auto Trim Mode: ‘) + (newState ? String(‘ON’) : String(‘OFF’)), ‘info’);”);
html += String(” } catch (error) {“);
html += String(” console.error(‘Error setting auto trim mode:’, error);”);
html += String(” showFeedback(String(‘Failed to change auto trim mode: ‘) + error.message, ‘error’);”);
html += String(” }”);
html += String(“}”);
html += String(“function updateAutoTrimToggleUI() {“);
html += String(” const toggleButton = document.getElementById(‘autoTrimToggle’);”);
html += String(” if (isAutoTrimEnabled) {“);
html += String(” toggleButton.innerText = ‘Auto Trim: ON’;”);
html += String(” toggleButton.classList.remove(‘off’);”);
html += String(” } else {“);
html += String(” toggleButton.innerText = ‘Auto Trim: OFF (Learn Mode)’;”);
html += String(” toggleButton.classList.add(‘off’);”);
html += String(” }”);
html += String(“}”);
html += String(“document.addEventListener(‘DOMContentLoaded’, () => {“);
html += String(” document.getElementById(‘toggleTempUnit’).innerText = isFahrenheit ? String(‘C’) : String(‘F’);”);
html += String(” const ctxCal = document.getElementById(‘calChart’).getContext(‘2d’);”);
html += String(” calChart = new Chart(ctxCal, {“);
html += String(” type:’line’,”);
html += String(” data:{ labels:[], datasets:[{“);
html += String(” label:String(‘Trim Voltage (V)’),”);
html += String(” borderColor:String(‘rgba(52, 152, 219, 1)’),”);
html += String(” backgroundColor:String(‘rgba(52, 152, 219, 0.2)’),”);
html += String(” fill:true, tension: 0.3, pointRadius: 5, pointBackgroundColor: String(‘rgba(52, 152, 219, 1)’), pointBorderColor: ‘#fff’, pointHoverRadius: 7, data:[ ]”);
html += String(” }] },”);
html += String(” options:{“);
html += String(” responsive: true, maintainAspectRatio: false,”);
html += String(” scales:{“);
html += String(” x:{ “);
html += String(” type: ‘linear’, “); // Explicitly set x-axis type to linear
html += String(” title:{ display:true, text:String(‘Temp (°C)’), font: { size: 14, weight: ‘bold’ } }, grid: { display: false }, ticks: { callback: function(value) { return String(value.toFixed(1)) + ‘ °C’; } } },”);
html += String(” y:{ title:{ display:true, text:String(‘Voltage (V)’), font: { size: 14, weight: ‘bold’ } }, min:0, max:MAX_DAC_VOLTAGE, ticks: { callback: function(value) { return String(value.toFixed(2)) + ‘ V’; } } }”);
html += String(” },”);
html += String(” plugins: { tooltip: { callbacks: { label: function(context) { return String(‘Voltage: ‘) + context.parsed.y.toFixed(3) + ‘ V’; }, title: function(context) { return String(‘Temp: ‘) + context[0].parsed.x.toFixed(1) + ‘ °C’; } } }, legend: { display: false } }”);
html += String(” }”);
html += String(” });”);
html += String(” const ctxAge = document.getElementById(‘ageDriftChart’).getContext(‘2d’);”);
html += String(” ageDriftChart = new Chart(ctxAge, {“);
html += String(” type:’line’,”);
html += String(” data:{ labels:[], datasets:[{“);
html += String(” label:String(‘Trim Voltage (V)’),”);
html += String(” borderColor:String(‘rgba(230, 126, 34, 1)’),”);
html += String(” backgroundColor:String(‘rgba(230, 126, 34, 0.2)’),”);
html += String(” fill:true, tension: 0.3, pointRadius: 5, pointBackgroundColor: String(‘rgba(230, 126, 34, 1)’), pointBorderColor: ‘#fff’, pointHoverRadius: 7, data:[ ]”);
html += String(” }] },”);
html += String(” options:{“);
html += String(” responsive: true, maintainAspectRatio: false,”);
html += String(” scales:{“);
html += String(” x:{ title:{ display:true, text:String(‘OCXO Age’), font: { size: 14, weight: ‘bold’ } }, grid: { display: false } },”);
html += String(” y:{ title:{ display:true, text:String(‘Voltage (V)’), font: { size: 14, weight: ‘bold’ } }, min:0, max:MAX_DAC_VOLTAGE, ticks: { callback: function(value) { return String(value.toFixed(2)) + ‘ V’; } } }”);
html += String(” },”);
html += String(” plugins: { tooltip: { callbacks: { label: function(context) { return String(‘Voltage: ‘) + context.parsed.y.toFixed(3) + ‘ V’; }, title: function(context) { return String(‘Age: ‘) + context.label; } } }, legend: { display: false } }”);
html += String(” }”);
html += String(” });”);
html += String(” upd();”);
html += String(” setInterval(upd, TEMP_INTERVAL);”);
html += String(” setInterval(updateUptimeAndAgeDisplay, 1000);”);
html += String(” updateSliderDisplay(document.getElementById(‘coarse’).value);”);
html += String(” viewTable();”);
html += String(” viewPlot();”);
html += String(” viewAgeDriftPlot();”);
html += String(“});”);
html += String(“</script>”);
html += String(“</body>”);
html += String(“</html>”);
server.send(200, “text/html”, html);
}
// ———————————————————————————————————————————————
// loadCalibration(): Loads calibration points for a given bank from EEPROM
// ———————————————————————————————————————————————
void loadCalibration(uint8_t bank) {
EEPROM.get(CAL_COUNT_ADDR(bank), calCount);
if (calCount > MAX_CAL_POINTS) { // Sanity check
calCount = 0;
}
for (uint8_t i = 0; i < calCount; i++) {
EEPROM.get(CAL_ENTRY_ADDR(bank) + i * sizeof(CalPoint), calPts[i]);
}
Serial.printf(“Loaded %u calibration points from bank %u.\n”, calCount, bank);
// Sort the calibration points by temperature (t10)
for (uint8_t i = 0; i < calCount – 1; i++) {
for (uint8_t j = i + 1; j < calCount; j++) {
if (calPts[i].t10 > calPts[j].t10) {
CalPoint temp = calPts[i];
calPts[i] = calPts[j];
calPts[j] = temp;
}
}
}
}
// ———————————————————————————————————————————————
// saveCalibrationPoint(): Saves a new calibration point to EEPROM
// ———————————————————————————————————————————————
void saveCalibrationPoint(uint8_t bank, uint16_t t10, uint16_t trim, uint64_t age_ms) {
if (calCount < MAX_CAL_POINTS) {
calPts[calCount] = {t10, trim, age_ms};
calCount++;
EEPROM.put(CAL_COUNT_ADDR(bank), calCount);
EEPROM.put(CAL_ENTRY_ADDR(bank) + (calCount – 1) * sizeof(CalPoint), calPts[calCount – 1]);
if (EEPROM.commit()) {
Serial.printf(“Saved calibration point: T=%u, Trim=%u, Age=%llu ms. Total: %u\n”, t10, trim, age_ms, calCount);
} else {
Serial.println(“EEPROM commit failed!”);
}
// Re-sort after adding a new point
for (uint8_t i = 0; i < calCount – 1; i++) {
for (uint8_t j = i + 1; j < calCount; j++) {
if (calPts[i].t10 > calPts[j].t10) {
CalPoint temp = calPts[i];
calPts[i] = calPts[j];
calPts[j] = temp;
}
}
}
} else {
Serial.println(“Calibration map full. Cannot save new point.”); // 50 should be enough
}
}
// ———————————————————————————————————————————————
// resetCalibration(): Clears all calibration points for a given bank
// ———————————————————————————————————————————————
void resetCalibration(uint8_t bank) {
calCount = 0;
EEPROM.put(CAL_COUNT_ADDR(bank), calCount);
// Optionally clear the actual data area to zeros, though not strictly necessary for functionality
// for (uint16_t i = 0; i < MAX_CAL_POINTS * sizeof(CalPoint); i++) {
// EEPROM.write(CAL_ENTRY_ADDR(bank) + i, 0);
// }
if (EEPROM.commit()) {
Serial.printf(“Calibration map for bank %u reset.\n”, bank);
} else {
Serial.println(“EEPROM commit failed!”);
}
}
// ———————————————————————————————————————————————
// lookupTrim(): Interpolates the trim value for a given temperature
// ———————————————————————————————————————————————
uint16_t lookupTrim(uint8_t bank, float tempC) {
if (calCount == 0) {
Serial.println(“No calibration points to interpolate.”);
return trimVal; // Return current trim if no points
}
uint16_t t10 = round(tempC * 10);
// If temperature is below the lowest calibration point
if (t10 <= calPts[0].t10) {
return calPts[0].trim;
}
// If temperature is above the highest calibration point
if (t10 >= calPts[calCount – 1].t10) {
return calPts[calCount – 1].trim;
}
// Find the two points for interpolation
for (uint8_t i = 0; i < calCount – 1; i++) {
if (t10 >= calPts[i].t10 && t10 <= calPts[i+1].t10) {
uint16_t t_low = calPts[i].t10;
uint16_t t_high = calPts[i+1].t10;
uint16_t trim_low = calPts[i].trim;
uint16_t trim_high = calPts[i+1].trim;
// Linear interpolation
// trim = trim_low + (trim_high – trim_low) * (t10 – t_low) / (t_high – t_low)
// Corrected: Cast to float for intermediate calculation to prevent integer truncation
uint16_t interpolatedTrim = trim_low + (uint16_t)(((float)(trim_high – trim_low) * (t10 – t_low)) / (t_high – t_low));
Serial.printf(“Interpolating for temp %f (%u): between (%u, %u) and (%u, %u). Result: %u\n”,
tempC, t10, t_low, trim_low, t_high, trim_high, interpolatedTrim);
return interpolatedTrim;
}
}
// Should not reach here if points are sorted and boundary conditions handled
Serial.println(“Lookup failed, returning current trim.”);
return trimVal;
}
// ———————————————————————————————————————————————
// SETUP
// ———————————————————————————————————————————————
void setup() {
Serial.begin(115200);
Serial.println(“setup() function entered.”); // Diagnostic: Confirm setup() is called
delay(100);
Serial.println(“\nStarting OCXO Monitor…”);
Wire.begin(); // Initialise I2C
dac.begin(0x60); // <<<<<<<<<<<<<< IMPORTANT DAC ADDRESS CHANGED TO 0x60 – Sniff yours if the temperature fails >>>>>>>>>>>>>>
sensors.begin(); // Initialise DS18B20 temperature sensor
EEPROM.begin(EEPROM_SIZE); // Initialise EEPROM
EEPROM.get(SSID_ADDR, storedSSID);
EEPROM.get(PASS_ADDR, storedPASS);
EEPROM.get(TRIM_ADDR, trimVal);
EEPROM.get(OCXO_ACCUM_UPTIME_ADDR, ocxoAccumulatedUptime);
EEPROM.get(AUTOTRIM_MODE_ADDR, autoTrimEnabled);
// COMMENT OUT OR REMOVE THE FOLLOWING TWO LINES TO PREVENT RESETTING TRIM ON EVERY BOOT.
// trimVal = MAX_DAC_COUNT / 2; // Force trimVal to a known good default
// EEPROM.put(TRIM_ADDR, trimVal);
// EEPROM.commit();
// The code assumes you have kept OVEN On during reboot. If the OVEN IS OFF then this is required.
// >>> END TEMPORARY RESET <<<
// Load calibration points for the current bank
loadCalibration(currentBank);
Serial.printf(“Loaded SSID: ‘%s’\n”, storedSSID);
Serial.printf(“Loaded Trim Value: %u (%.3f V)\n”, trimVal, (float)trimVal * DAC_VCC / (DAC_RES – 1));
Serial.printf(“Loaded OCXO Accumulated Uptime: %llu ms\n”, ocxoAccumulatedUptime);
Serial.printf(“Loaded Auto Trim Enabled: %s\n”, autoTrimEnabled ? “true” : “false”);
// Set initial DAC voltage
Serial.printf(“DEBUG: Before DAC setVoltage (initial setup) – trimVal: %u\n”, trimVal); // ADDED DEBUG PRINT
dac.setVoltage(trimVal, false);
// Connect to WiFi or start AP for configuration
if (strlen(storedSSID) == 0) {
Serial.println(“No WiFi credentials found. Starting AP mode.”);
WiFi.mode(WIFI_AP);
WiFi.softAP(CONFIG_AP_SSID, CONFIG_AP_PWD);
Serial.printf(“AP IP address: %s\n”, WiFi.softAPIP().toString().c_str());
server.on(“/”, handleConfig); // Serve config page on root in AP mode
server.on(“/config”, handleConfig);
} else {
Serial.printf(“Connecting to WiFi: %s\n”, storedSSID);
WiFi.mode(WIFI_STA);
WiFi.begin(storedSSID, storedPASS);
onGotIP = WiFi.onStationModeGotIP(onStationGotIP); // Register callback
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) { // Try for 15 seconds
delay(500);
Serial.print(“.”);
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf(“\nConnected to %s, IP: %s\n”, WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
MDNS.begin(“G5STO”);
} else {
Serial.println(“\nFailed to connect to STA. Starting AP mode for configuration.”);
WiFi.mode(WIFI_AP);
WiFi.softAP(CONFIG_AP_SSID, CONFIG_AP_PWD);
Serial.printf(“AP IP address: %s\n”, WiFi.softAPIP().toString().c_str());
server.on(“/”, handleConfig); // Serve config page on root in AP mode
server.on(“/config”, handleConfig);
}
}
// Web server routes (common for both STA and AP modes, but root might differ)
if (WiFi.getMode() == WIFI_STA) { // Only serve main UI if in STA mode
server.on(“/”, handleRoot);
}
server.on(“/data”, handleData);
server.on(“/set”, handleSet);
server.on(“/cal”, handleCal);
server.on(“/reset”, handleReset);
server.on(“/bank”, handleBank);
server.on(“/map”, handleMap);
server.on(“/mode”, handleMode);
server.begin();
Serial.println(“HTTP server started”);
lastEepromSaveMs = millis(); // Initialise last EEPROM save time
}
// ———————————————————————————————————————————————
// LOOP
// ———————————————————————————————————————————————
void loop() {
server.handleClient();
MDNS.update();
// Temperature reading and auto-trim logic
if (millis() – lastTempMs > TEMP_INTERVAL) {
sensors.requestTemperatures();
currentTemp = sensors.getTempCByIndex(0);
currentRSSI = WiFi.RSSI(); // Update RSSI
lastTempMs = millis();
Serial.printf(“Temp: %.2f C, RSSI: %d dBm\n”, currentTemp, currentRSSI);
if (autoTrimEnabled && !isnan(currentTemp)) {
uint16_t newTrim = lookupTrim(currentBank, currentTemp);
if (newTrim != trimVal) { // Only update if value changes
trimVal = newTrim;
Serial.printf(“DEBUG: Auto-trim – Before DAC setVoltage – trimVal: %u\n”, trimVal); // ADDED DEBUG PRINT
dac.setVoltage(trimVal, false);
EEPROM.put(TRIM_ADDR, trimVal); // Save immediately on auto-trim change
EEPROM.commit();
Serial.printf(“Auto-trim: DAC set to %u (%.3f V)\n”, trimVal, (float)trimVal * DAC_VCC / (DAC_RES – 1));
}
}
}
// Periodically save accumulated OCXO uptime to EEPROM
if (millis() – lastEepromSaveMs > EEPROM_SAVE_INTERVAL) {
ocxoAccumulatedUptime += (millis() – lastEepromSaveMs);
EEPROM.put(OCXO_ACCUM_UPTIME_ADDR, ocxoAccumulatedUptime);
if (EEPROM.commit()) {
Serial.printf(“OCXO Accumulated Uptime saved: %llu ms\n”, ocxoAccumulatedUptime); // Why? was to aid faster accumulation of temperature change
} else {
Serial.println(“EEPROM commit failed for uptime!”);
}
lastEepromSaveMs = millis();
}
}
*****************************************************************************************************************