So, here we are, putting the final(-ish) touches on our smart fan controller project. What started as a simple temperature-based fan switch has evolved into something much more sophisticated. Now, we’re talking temperature offsets, data logging, real-time charts, and even NTP time synchronization.
If you missed the original posts where this project started, make sure you check them out:
- Building a Smarter Fan Controller for Your Rack
- Enhancing Your Fan Controller: Adding a Web Interface
- Adding OTA Updates to Your ESP8266 Fan Controller
Let’s dive into these new features and see what’s under the hood.
Temperature Offset Adjustment
First up, temperature offset. I noticed how our sensor’s reading didn’t quite match that fancy external display we’ve got. Well, now you can calibrate your readings with a simple offset adjustment. It’s like giving your sensor a pair of glasses – it finally sees things clearly! In our case, I don’t know the accuracy of the external display on the rack, but this still becomes a useful feature as you could calibrate on the fly with a known accurate sensor as well.
float TEMP_OFFSET = -0.5; // Temperature offset to match external display
void handleSetTempOffset() {
String tempOffset = server.arg("tempOffset");
TEMP_OFFSET = tempOffset.toFloat();
server.send(200, "text/plain", "OK");
}
Data Logging and Chart Display
Next, we’ve got data logging and chart displays – because who doesn’t love a good graph? With this update, your temperature data is logged over time, the logs rotate out, and you can visualize it on the web interface.
void storeData(float temp) {
const int maxEntries = 300; // Maximum number of entries to keep
String newData = String(timeClient.getEpochTime()) + "," + String(temp, 1);
// Read existing data
File file = SPIFFS.open("/data.txt", "r");
String data = "";
if (file) {
while (file.available()) {
data += (char)file.read();
}
file.close();
}
// Split the data into entries based on the semicolon delimiter
int numEntries = 0;
int pos = 0;
while ((pos = data.indexOf(';', pos)) != -1) {
numEntries++;
pos++;
}
// If we have too many entries, remove the oldest ones
if (numEntries >= maxEntries) {
int trimPos = data.indexOf(';') + 1;
data = data.substring(trimPos);
}
// Append the new entry
if (data.length() > 0) {
data += ";";
}
data += newData;
// Write the updated data back to the file
file = SPIFFS.open("/data.txt", "w");
if (file) {
file.print(data);
file.close();
} else {
logDebug("Failed to open data file for writing");
}
}
void handleChartData() {
if (!SPIFFS.exists("/data.txt")) {
logDebug("File not found: /data.txt");
server.send(404, "text/plain", "File Not Found");
return;
}
File file = SPIFFS.open("/data.txt", "r");
String data = "";
if (file) {
while (file.available()) {
data += (char)file.read();
}
file.close();
} else {
logDebug("Failed to open data file: /data.txt");
server.send(500, "text/plain", "Failed to open data file");
return;
}
// Send the data to the client
server.send(200, "text/plain", data);
}
NTP Time Synchronization
We also brought in NTP time synchronization. Now, all those data points can be accurately timestamped, ensuring you know exactly when things heated up. No more guessing when that temperature spike happened.
// NTP settings
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "1.ca.pool.ntp.org", 0, 60000); // UTC time, update every 60 seconds
void setup() {
...
// Initialize NTP
timeClient.begin();
// Synchronize time with NTP server
while(!timeClient.update()) {
timeClient.forceUpdate();
}
// Log the initial time synchronization
String currentTime = timeClient.getFormattedTime();
logDebug("Time synchronized: " + currentTime);
...
}
void loop() {
...
static unsigned long lastNTPUpdateTime = 0;
const unsigned long NTP_UPDATE_INTERVAL = 3600000; // Update every 60 minutes (3600000 ms)
// Update NTP time periodically
if (millis() - lastNTPUpdateTime >= NTP_UPDATE_INTERVAL) {
timeClient.update();
lastNTPUpdateTime = millis();
}
...
}
Enhanced Web Interface
We’ve also given our web interface a much-needed facelift. It’s got Bootstrap for that sleek, responsive look and now includes real-time updates, manual controls, and even the ability to set thresholds right from your browser.
void handleRoot() {
float temp = dht.readTemperature() + TEMP_OFFSET;
float humidity = dht.readHumidity();
String fanStatus = fanOn ? "ON" : "OFF";
String html = "<html><head><meta charset=\"UTF-8\">";
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">";
html += "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">";
html += "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js\"></script>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; }";
html += "h1 { color: #333; }";
html += ".btn { border-radius: 0; }"; /* Make buttons square */
html += ".container { max-width: 1000px; margin-top: 20px; }";
html += ".form-group { margin-bottom: 10px; }";
html += "canvas { display: block; margin: 0 auto; }";
html += "#logContent { background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; white-space: pre-wrap; height: 200px; overflow-y: scroll; }";
html += ".inline-input { width: 95px; display: inline-block; }"; /* Adjusted width */
html += ".inline-label { display: inline-block; margin-right: 30px; }"; /* Adjusted margin */
html += "</style>";
html += "<script>";
html += "function updateData() {";
html += " fetch('/update').then(response => response.json()).then(data => {";
html += " document.getElementById('temp').innerText = data.temperature + ' °C';";
html += " document.getElementById('humidity').innerText = data.humidity + ' %';";
html += " document.getElementById('fanStatus').innerText = data.fanStatus;";
html += " });";
html += " fetchLog();";
html += " fetchChartData();";
html += "}";
html += "function controlFan(command) {";
html += " fetch('/control?command=' + command).then(() => updateData());";
html += "}";
html += "function setControls() {";
html += " const onThreshold = document.getElementById('onThreshold').value;";
html += " const offThreshold = document.getElementById('offThreshold').value;";
html += " const tempOffset = document.getElementById('tempOffset').value;";
html += " fetch(`/setThresholds?onThreshold=${onThreshold}&offThreshold=${offThreshold}`).then(() => {";
html += " fetch(`/setTempOffset?tempOffset=${tempOffset}`).then(() => updateData());";
html += " });";
html += "}";
html += "function fetchChartData() {";
html += " fetch('/chart').then(response => response.text()).then(data => {";
html += " const entries = data.split(';');"; // Split data by semicolon
html += " const labels = [];"; // For timestamps
html += " const tempData = [];"; // For temperatures";
html += " for (let i = 0; i < entries.length; i++) {";
html += " const parts = entries[i].split(',');"; // Split each entry into timestamp and temperature
html += " const timestamp = new Date(parseInt(parts[0]) * 1000);"; // Convert epoch to milliseconds and then to Date object
html += " labels.push(timestamp);"; // Use Date object as label";
html += " tempData.push(parseFloat(parts[1]));"; // Temperature
html += " }";
html += " new Chart(document.getElementById('chart'), {";
html += " type: 'line',";
html += " data: { labels: labels, datasets: [{ label: 'Temperature (°C)', data: tempData }] },";
html += " options: {";
html += " animation: false,"; // Disable animations";
html += " scales: {";
html += " xAxes: [{ type: 'time', time: { unit: 'minute', tooltipFormat: 'll HH:mm:ss', displayFormats: { minute: 'HH:mm' }}}],";
html += " yAxes: [{ type: 'linear', position: 'left' }]";
html += " }";
html += " }";
html += " });";
html += " }).catch(error => console.error('Error fetching chart data:', error));";
html += "}";
html += "function fetchLog() {";
html += " fetch('/log.txt').then(response => response.text()).then(data => {";
html += " document.getElementById('logContent').innerText = data;";
html += " });";
html += "}";
html += "function clearLog() {";
html += " fetch('/clearLog').then(() => {";
html += " document.getElementById('logContent').innerText = '';";
html += " });";
html += "}";
html += "setInterval(updateData, 2000);";
html += "</script>";
html += "</head><body onload='updateData();'>";
html += "<div class='container'>";
html += "<h1 class='text-center'>Fan Controller</h1>";
html += "<canvas id='chart' width='300' height='150'></canvas>";
html += "<div class='form-group text-center'>";
html += "<span class='inline-label'>Current Temperature: <span id='temp'>" + String(temp) + " °C</span></span>";
html += "<span class='inline-label'>Current Humidity: <span id='humidity'>" + String(humidity) + " %</span></span>";
html += "<span class='inline-label'>Fan Status: <span id='fanStatus'>" + fanStatus + "</span></span>";
html += "</div>";
html += "<div class='form-group text-center'>";
html += "<label class='inline-label'>On Threshold: <input type='number' id='onThreshold' value='" + String(ON_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
html += "<label class='inline-label'>Off Threshold: <input type='number' id='offThreshold' value='" + String(OFF_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
html += "<label class='inline-label'>Temperature Offset: <input type='number' id='tempOffset' value='" + String(TEMP_OFFSET) + "' class='form-control inline-input'> °C</label>";
html += "</div>";
html += "<button onclick='setControls()' class='btn btn-primary btn-block'>Set Thresholds and Offset</button>";
html += "<div class='d-flex justify-content-between mt-3'>";
html += "<button onclick='controlFan(\"ON\")' class='btn btn-success flex-fill mr-1'>Turn Fan ON</button>";
html += "<button onclick='controlFan(\"OFF\")' class='btn btn-danger flex-fill ml-1'>Turn Fan OFF</button>";
html += "</div>";
html += "<pre id='logContent' class='mt-3'></pre>";
html += "<button onclick='clearLog()' class='btn btn-warning btn-block mt-3'>Clear Log</button>";
html += "</div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
Manual Fan Control & LED Indicator
Sometimes, you just need to take control. That’s why we added manual fan control through the web interface. Plus, we synced the built-in LED with the fan status.
void handleFanControl() {
String command = server.arg("command");
if (command == "ON") {
digitalWrite(RELAY_PIN, LOW); // Turn on the fan
digitalWrite(BUILTIN_LED, LOW); // Sync LED state with fan
fanOn = true;
} else if (command == "OFF") {
digitalWrite(RELAY_PIN, HIGH); // Turn off the fan
digitalWrite(BUILTIN_LED, HIGH); // Sync LED state with fan
fanOn = false;
}
server.send(200, "text/plain", "OK");
}
Additional Changes
We didn’t stop there. A few other tweaks make this system even better:
Built-in LED Control: The ESP8266’s LED now lights up to show the fan status.
Clear Log and Data Feature: Need a fresh start? You can clear logs and data files directly from the interface.
Improved Error Handling: We beefed up the error handling so your system can keep chugging along, even if something goes awry.
Humidity Display: We’ve also added real-time humidity monitoring.
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DHT.h>
#include <ArduinoOTA.h>
#include <FS.h>
#include <NTPClient.h>
#define DHTPIN 2 // GPIO2 corresponds to D4 on the ESP8266
#define DHTTYPE DHT11 // Define the sensor type
#define RELAY_PIN 5 // GPIO5 corresponds to D1 on the ESP8266
#define BUILTIN_LED 2 // GPIO2 corresponds to the built-in LED on the ESP8266
const char* ssid = "YOUR SSID";
const char* password = "YOUR WIFI PASSWORD";
// NTP settings
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "1.ca.pool.ntp.org", 0, 60000); // UTC time, update every 60 seconds
DHT dht(DHTPIN, DHTTYPE);
ESP8266WebServer server(80);
bool fanOn = false;
float ON_THRESHOLD = 30.0; // Temperature to turn the fan on
float OFF_THRESHOLD = 28.0; // Temperature to turn the fan off
float TEMP_OFFSET = -0.5; // Temperature offset to match external display
float lastLoggedTemp = 0;
void logDebug(String message) {
//Serial.println(message); // Output to serial console
File logFile = SPIFFS.open("/log.txt", "a");
if (logFile) {
logFile.println(message);
logFile.close();
}
}
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUILTIN_LED, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Ensure the fan is initially off
digitalWrite(BUILTIN_LED, HIGH); // Ensure the LED is off
dht.begin();
if (!SPIFFS.begin()) {
Serial.println("Failed to mount file system");
return;
}
// Create initial data file if it doesn't exist
if (!SPIFFS.exists("/data.txt")) {
File dataFile = SPIFFS.open("/data.txt", "w");
if (!dataFile) {
logDebug("Failed to create data file");
} else {
dataFile.close();
logDebug("Data file created successfully");
}
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
logDebug("Connecting to WiFi...");
}
logDebug("Connected to WiFi");
// Initialize NTP
timeClient.begin();
// Synchronize time with NTP server
while (!timeClient.update()) {
timeClient.forceUpdate();
}
// Log the initial time synchronization
String currentTime = timeClient.getFormattedTime();
logDebug("Time synchronized: " + currentTime);
server.on("/", handleRoot);
server.on("/update", handleUpdate);
server.on("/control", handleFanControl);
server.on("/chart", handleChartData);
server.on("/setThresholds", handleSetThresholds);
server.on("/setTempOffset", handleSetTempOffset);
server.on("/log.txt", handleLogFile);
server.on("/data.txt", handleChartData);
server.on("/clearLog", handleClearLog);
server.begin();
logDebug("HTTP server started");
logDebug("IP Address: " + WiFi.localIP().toString());
// Start OTA
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_SPIFFS
type = "filesystem";
}
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.println("Progress: " + String((progress / (total / 100))) + "%");
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.println("Error[" + String(error) + "]: " + (error == OTA_AUTH_ERROR ? "Auth Failed" : error == OTA_BEGIN_ERROR ? "Begin Failed"
: error == OTA_CONNECT_ERROR ? "Connect Failed"
: error == OTA_RECEIVE_ERROR ? "Receive Failed"
: "End Failed"));
});
ArduinoOTA.begin();
}
void loop() {
server.handleClient();
ArduinoOTA.handle(); // Handle OTA updates
static unsigned long lastReadTime = 0;
static unsigned long lastNTPUpdateTime = 0;
const unsigned long NTP_UPDATE_INTERVAL = 3600000; // Update every 60 minutes (3600000 ms)
// Update NTP time at triggered intervals
if (millis() - lastNTPUpdateTime >= NTP_UPDATE_INTERVAL) {
timeClient.update(); // Update the time
lastNTPUpdateTime = millis();
}
if (millis() - lastReadTime >= 2000) { // Read every 2 seconds
float temp = dht.readTemperature() + TEMP_OFFSET;
float humidity = dht.readHumidity();
if (!isnan(temp) && !isnan(humidity)) {
if (abs(temp - lastLoggedTemp) >= 1.0) {
logDebug("Temperature: " + String(temp) + " °C, Humidity: " + String(humidity) + " %");
lastLoggedTemp = temp;
}
storeData(temp);
if (!fanOn && temp > ON_THRESHOLD) {
digitalWrite(RELAY_PIN, LOW); // Turn on the fan
digitalWrite(BUILTIN_LED, LOW); // Turn off the built-in LED (indicating fan is on)
fanOn = true;
logDebug("Fan turned ON automatically");
} else if (fanOn && temp < OFF_THRESHOLD) {
digitalWrite(RELAY_PIN, HIGH); // Turn off the fan
digitalWrite(BUILTIN_LED, HIGH); // Turn on the built-in LED (indicating fan is off)
fanOn = false;
logDebug("Fan turned OFF automatically");
}
} else {
logDebug("Failed to read from DHT sensor!");
}
lastReadTime = millis();
}
}
void handleRoot() {
float temp = dht.readTemperature() + TEMP_OFFSET;
float humidity = dht.readHumidity();
String fanStatus = fanOn ? "ON" : "OFF";
String html = "<html><head><meta charset=\"UTF-8\">";
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">";
html += "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">";
html += "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js\"></script>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; }";
html += "h1 { color: #333; }";
html += ".btn { border-radius: 0; }"; /* Make buttons square */
html += ".container { max-width: 1000px; margin-top: 20px; }";
html += ".form-group { margin-bottom: 10px; }";
html += "canvas { display: block; margin: 0 auto; }";
html += "#logContent { background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; white-space: pre-wrap; height: 200px; overflow-y: scroll; }";
html += ".inline-input { width: 95px; display: inline-block; }"; /* Adjusted width */
html += ".inline-label { display: inline-block; margin-right: 30px; }"; /* Adjusted margin */
html += "</style>";
html += "<script>";
html += "function updateData() {";
html += " fetch('/update').then(response => response.json()).then(data => {";
html += " document.getElementById('temp').innerText = data.temperature + ' °C';";
html += " document.getElementById('humidity').innerText = data.humidity + ' %';";
html += " document.getElementById('fanStatus').innerText = data.fanStatus;";
html += " });";
html += " fetchLog();";
html += " fetchChartData();";
html += "}";
html += "function controlFan(command) {";
html += " fetch('/control?command=' + command).then(() => updateData());";
html += "}";
html += "function setControls() {";
html += " const onThreshold = document.getElementById('onThreshold').value;";
html += " const offThreshold = document.getElementById('offThreshold').value;";
html += " const tempOffset = document.getElementById('tempOffset').value;";
html += " fetch(`/setThresholds?onThreshold=${onThreshold}&offThreshold=${offThreshold}`).then(() => {";
html += " fetch(`/setTempOffset?tempOffset=${tempOffset}`).then(() => updateData());";
html += " });";
html += "}";
html += "function fetchChartData() {";
html += " fetch('/chart').then(response => response.text()).then(data => {";
html += " const entries = data.split(';');"; // Split data by semicolon
html += " const labels = [];"; // For timestamps
html += " const tempData = [];"; // For temperatures";
html += " for (let i = 0; i < entries.length; i++) {";
html += " const parts = entries[i].split(',');"; // Split each entry into timestamp and temperature
html += " const timestamp = new Date(parseInt(parts[0]) * 1000);"; // Convert epoch to milliseconds and then to Date object
html += " labels.push(timestamp);"; // Use Date object as label";
html += " tempData.push(parseFloat(parts[1]));"; // Temperature
html += " }";
html += " new Chart(document.getElementById('chart'), {";
html += " type: 'line',";
html += " data: { labels: labels, datasets: [{ label: 'Temperature (°C)', data: tempData }] },";
html += " options: {";
html += " animation: false,"; // Disable animations";
html += " scales: {";
html += " xAxes: [{ type: 'time', time: { unit: 'minute', tooltipFormat: 'll HH:mm:ss', displayFormats: { minute: 'HH:mm' }}}],";
html += " yAxes: [{ type: 'linear', position: 'left' }]";
html += " }";
html += " }";
html += " });";
html += " }).catch(error => console.error('Error fetching chart data:', error));";
html += "}";
html += "function fetchLog() {";
html += " fetch('/log.txt').then(response => response.text()).then(data => {";
html += " document.getElementById('logContent').innerText = data;";
html += " });";
html += "}";
html += "function clearLog() {";
html += " fetch('/clearLog').then(() => {";
html += " document.getElementById('logContent').innerText = '';";
html += " });";
html += "}";
html += "setInterval(updateData, 2000);";
html += "</script>";
html += "</head><body onload='updateData();'>";
html += "<div class='container'>";
html += "<h1 class='text-center'>Fan Controller</h1>";
html += "<canvas id='chart' width='300' height='150'></canvas>";
html += "<div class='form-group text-center'>";
html += "<span class='inline-label'>Current Temperature: <span id='temp'>" + String(temp) + " °C</span></span>";
html += "<span class='inline-label'>Current Humidity: <span id='humidity'>" + String(humidity) + " %</span></span>";
html += "<span class='inline-label'>Fan Status: <span id='fanStatus'>" + fanStatus + "</span></span>";
html += "</div>";
html += "<div class='form-group text-center'>";
html += "<label class='inline-label'>On Threshold: <input type='number' id='onThreshold' value='" + String(ON_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
html += "<label class='inline-label'>Off Threshold: <input type='number' id='offThreshold' value='" + String(OFF_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
html += "<label class='inline-label'>Temperature Offset: <input type='number' id='tempOffset' value='" + String(TEMP_OFFSET) + "' class='form-control inline-input'> °C</label>";
html += "</div>";
html += "<button onclick='setControls()' class='btn btn-primary btn-block'>Set Thresholds and Offset</button>";
html += "<div class='d-flex justify-content-between mt-3'>";
html += "<button onclick='controlFan(\"ON\")' class='btn btn-success flex-fill mr-1'>Turn Fan ON</button>";
html += "<button onclick='controlFan(\"OFF\")' class='btn btn-danger flex-fill ml-1'>Turn Fan OFF</button>";
html += "</div>";
html += "<pre id='logContent' class='mt-3'></pre>";
html += "<button onclick='clearLog()' class='btn btn-warning btn-block mt-3'>Clear Log</button>";
html += "</div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleUpdate() {
float temp = dht.readTemperature() + TEMP_OFFSET;
float humidity = dht.readHumidity();
String fanStatus = fanOn ? "ON" : "OFF";
String json = "{";
json += "\"temperature\": " + String(temp) + ",";
json += "\"humidity\": " + String(humidity) + ",";
json += "\"fanStatus\": \"" + fanStatus + "\"";
json += "}";
server.send(200, "application/json", json);
}
void handleFanControl() {
String command = server.arg("command");
logDebug("Fan control command received: " + command);
if (command == "ON") {
digitalWrite(RELAY_PIN, LOW);
digitalWrite(BUILTIN_LED, LOW); // Sync LED state with relay (fan on)
fanOn = true;
logDebug("Fan turned ON");
} else if (command == "OFF") {
digitalWrite(RELAY_PIN, HIGH);
digitalWrite(BUILTIN_LED, HIGH); // Sync LED state with relay (fan off)
fanOn = false;
logDebug("Fan turned OFF");
}
server.send(200, "text/plain", "OK");
}
void handleSetThresholds() {
String onThreshold = server.arg("onThreshold");
String offThreshold = server.arg("offThreshold");
logDebug("Set thresholds command received: onThreshold=" + onThreshold + ", offThreshold=" + offThreshold);
ON_THRESHOLD = onThreshold.toFloat();
OFF_THRESHOLD = offThreshold.toFloat();
// Immediately evaluate and update fan state based on new thresholds
float temp = dht.readTemperature() + TEMP_OFFSET;
if (!isnan(temp)) {
if (fanOn && temp < OFF_THRESHOLD) {
digitalWrite(RELAY_PIN, HIGH); // Turn off the fan
digitalWrite(BUILTIN_LED, HIGH); // Sync LED state with relay (fan off)
fanOn = false;
logDebug("Fan turned OFF due to new threshold");
} else if (!fanOn && temp > ON_THRESHOLD) {
digitalWrite(RELAY_PIN, LOW); // Turn on the fan
digitalWrite(BUILTIN_LED, LOW); // Sync LED state with relay (fan on)
fanOn = true;
logDebug("Fan turned ON due to new threshold");
}
}
server.send(200, "text/plain", "OK");
}
void handleSetTempOffset() {
String tempOffset = server.arg("tempOffset");
logDebug("Set temperature offset command received: tempOffset=" + tempOffset);
TEMP_OFFSET = tempOffset.toFloat();
server.send(200, "text/plain", "OK");
}
void handleChartData() {
if (!SPIFFS.exists("/data.txt")) {
logDebug("File not found: /data.txt");
server.send(404, "text/plain", "File Not Found");
return;
}
File file = SPIFFS.open("/data.txt", "r");
String data = "";
if (file) {
while (file.available()) {
data += (char)file.read();
}
file.close();
} else {
logDebug("Failed to open data file: /data.txt");
server.send(500, "text/plain", "Failed to open data file");
return;
}
// Send the data to the client
server.send(200, "text/plain", data);
}
void handleLogFile() {
if (!SPIFFS.exists("/log.txt")) {
server.send(200, "text/plain", "");
return;
}
File logFile = SPIFFS.open("/log.txt", "r");
if (!logFile) {
server.send(500, "text/plain", "Failed to open log file");
return;
}
String log = "";
while (logFile.available()) {
log += logFile.readStringUntil('\n');
}
logFile.close();
server.send(200, "text/plain", log.length() > 0 ? log : " ");
}
void handleClearLog() {
if (SPIFFS.exists("/log.txt")) {
SPIFFS.remove("/log.txt");
}
if (SPIFFS.exists("/data.txt")) {
SPIFFS.remove("/data.txt");
// Recreate the data.txt file to prevent issues with non-existent file
File dataFile = SPIFFS.open("/data.txt", "w");
if (!dataFile) {
logDebug("Failed to create data file");
} else {
//dataFile.println("0.0");
dataFile.close();
logDebug("Data file created successfully");
}
}
logDebug("Log file cleared");
server.send(200, "text/plain", "Log cleared");
}
void storeData(float temp) {
const int maxEntries = 300; // Maximum number of entries to keep
String newData = String(timeClient.getEpochTime()) + "," + String(temp, 1);
// Read existing data
File file = SPIFFS.open("/data.txt", "r");
String data = "";
if (file) {
while (file.available()) {
data += (char)file.read();
}
file.close();
}
// Split the data into entries based on the semicolon delimiter
int numEntries = 0;
int pos = 0;
while ((pos = data.indexOf(';', pos)) != -1) {
numEntries++;
pos++;
}
// If we have too many entries, remove the oldest ones
if (numEntries >= maxEntries) {
int trimPos = data.indexOf(';') + 1;
data = data.substring(trimPos);
}
// Append the new entry
if (data.length() > 0) {
data += ";";
}
data += newData;
// Write the updated data back to the file
file = SPIFFS.open("/data.txt", "w");
if (file) {
file.print(data);
file.close();
} else {
logDebug("Failed to open data file for writing");
}
}
With these updates, our smart fan controller is now a complete package. It monitors, it logs, it visualizes, and it gives you full control – all in one neat little system. Whether you’re using it for a server rack, a greenhouse, or anywhere else, this controller is ready to handle it all.
Woah! Dude! Far out man!