/*
ESP32 16-Sensor Logger - Chart Loading Fixed
*/
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <SD.h>
#include <SPI.h>
#include <time.h>
#include <Arduino_JSON.h>
#define SD_CS 5
#define SD_MOSI 23
#define SD_MISO 19
#define SD_SCK 18
const char* setupFile = "/setup.csv";
const int MAX_POINTS = 64;
const int NUM_SENSORS = 16;
AsyncWebServer server(80);
String sensorNames[NUM_SENSORS];
String ssidStr = "Wokwi-GUEST";
String passStr = "";
bool visible[NUM_SENSORS];
String getCurrentDateFile() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return "20260715.csv";
char buf[20];
sprintf(buf, "%04d%02d%02d.csv", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
return String(buf);
}
void setDefaultNames() {
for (int i = 0; i < NUM_SENSORS; i++) {
sensorNames[i] = "Sensor" + String(i + 1);
visible[i] = true;
}
}
void initSD() {
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS)) {
Serial.println("SD Card failed!");
return;
}
Serial.println("SD Card initialized.");
if (!SD.exists(setupFile)) saveSetup();
else loadSetup();
}
void loadSetup() {
File f = SD.open(setupFile, FILE_READ);
if (!f) return;
String content = f.readString();
f.close();
JSONVar doc = JSON.parse(content);
if (JSON.typeof(doc) == "undefined") return;
ssidStr = (const char*)doc["ssid"];
passStr = (const char*)doc["password"];
JSONVar names = doc["names"];
JSONVar vis = doc["visible"];
for (int i = 0; i < NUM_SENSORS; i++) {
if (i < names.length()) sensorNames[i] = (const char*)names[i];
if (i < vis.length()) visible[i] = (bool)vis[i];
}
}
void saveSetup() {
JSONVar doc;
doc["ssid"] = ssidStr;
doc["password"] = passStr;
JSONVar names, vis;
for (int i = 0; i < NUM_SENSORS; i++) {
names[i] = sensorNames[i];
vis[i] = visible[i];
}
doc["names"] = names;
doc["visible"] = vis;
File f = SD.open(setupFile, FILE_WRITE);
if (f) {
f.print(JSON.stringify(doc));
f.close();
}
}
String getTimestamp() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return "1970-01-01 00:00:00";
char buf[20];
sprintf(buf, "%04d-%02d-%02d %02d:%02d:%02d",
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
return String(buf);
}
void logData(float values[]) {
String ts = getTimestamp();
Serial.println("Logged: " + ts);
String line = ts;
for (int i = 0; i < NUM_SENSORS; i++) {
line += "," + String(values[i], 2);
}
String todayFile = getCurrentDateFile();
File file = SD.open(todayFile.c_str(), FILE_READ);
String lines[MAX_POINTS + 2];
int count = 0;
if (file) {
while (file.available() && count < MAX_POINTS + 1) {
lines[count++] = file.readStringUntil('\n');
}
file.close();
}
if (count - 1 >= MAX_POINTS) {
for (int i = 1; i < count - 1; i++) lines[i] = lines[i + 1];
count--;
}
if (count == 0) {
String header = "timestamp";
for (int i = 0; i < NUM_SENSORS; i++) header += "," + sensorNames[i];
lines[count++] = header;
}
lines[count++] = line;
file = SD.open(todayFile.c_str(), FILE_WRITE);
if (file) {
for (int i = 0; i < count; i++) file.println(lines[i]);
file.close();
}
}
void setup() {
Serial.begin(115200);
setDefaultNames();
initSD();
WiFi.begin(ssidStr.c_str(), passStr.c_str());
Serial.print("Connecting to WiFi");
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.status() == WL_CONNECTED ? "\nConnected! IP: " + WiFi.localIP().toString() : "\nWiFi failed");
configTime(-18000, 0, "pool.ntp.org");
setenv("TZ", "CST6CDT,M3.2.0,M11.1.0", 1);
tzset();
// Main Page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String visArray = "[";
for (int i = 0; i < NUM_SENSORS; i++) {
visArray += visible[i] ? "true" : "false";
if (i < NUM_SENSORS - 1) visArray += ",";
}
visArray += "]";
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP32 Logger</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://code.highcharts.com/highcharts.js"></script>
<style>
body {font-family:Arial,sans-serif;background:#f4f4f4;margin:0;padding:20px;}
.container {max-width:1450px;margin:auto;background:white;padding:25px;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.1);}
.controls {display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin:15px 0;align-items:center;}
.chart-container {height:580px;margin-top:20px;}
.status {margin-top:25px;padding:12px;background:#e8f4fd;border-radius:6px;text-align:center;font-size:0.95em;}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 16-Sensor Logger</h1>
<div class="controls">
<button onclick="manualRefresh()">Refresh Now</button>
<button onclick="window.location='/setup'">Setup</button>
<button onclick="downloadCSV()">Download CSV</button>
<label>Date: <input type="date" id="logDate" onchange="loadSelectedFile()"></label>
<label>Start: <input type="time" id="startTime"></label>
<label>End: <input type="time" id="endTime"></label>
</div>
<div id="chart-container" class="chart-container"></div>
<div class="status" id="status">Loading...</div>
</div>
<script>
let chart, allData = [], currentNames = [];
const visibleSensors = )rawliteral" + visArray + R"rawliteral(;
let currentFile = '';
function getTodayDate() {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth()+1).padStart(2,'0');
const d = String(now.getDate()).padStart(2,'0');
return `${y}-${m}-${d}`;
}
async function loadSelectedFile() {
const dateInput = document.getElementById('logDate').value;
if (!dateInput) return;
currentFile = dateInput.replace(/-/g,'') + '.csv';
try {
const r = await fetch('/file?file=' + encodeURIComponent(currentFile));
if (!r.ok) throw new Error('File not found');
const csv = await r.text();
parseCSV(csv);
updateChart();
} catch(e) {
console.error(e);
document.getElementById('status').innerHTML += ' (File not found)';
}
}
async function manualRefresh() {
document.getElementById('logDate').value = getTodayDate();
loadSelectedFile();
}
function parseCSV(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',');
currentNames = headers.slice(1);
allData = [];
for (let i=1; i<lines.length; i++) {
const v = lines[i].split(',');
if (v.length < 2) continue;
let e = {timestamp: v[0]};
for (let j=1; j<headers.length; j++) e[headers[j]] = parseFloat(v[j]) || 0;
allData.push(e);
}
}
function updateChart() {
const filtered = allData.filter(p => {
const s = document.getElementById('startTime').value;
const e = document.getElementById('endTime').value;
if (s && p.timestamp.split(' ')[1] < s) return false;
if (e && p.timestamp.split(' ')[1] > e) return false;
return true;
});
const series = [];
currentNames.forEach((name, i) => {
if (visibleSensors[i]) {
series.push({
name: name,
data: filtered.map(p => [new Date(p.timestamp.replace(' ','T') + '-05:00').getTime(), p[name]])
});
}
});
if (chart) chart.destroy();
chart = Highcharts.chart('chart-container', {
title: {text: 'Sensor Data'},
xAxis: {type: 'datetime'},
yAxis: {title: {text: 'Value'}},
series: series
});
}
function updateStatus() {
const now = new Date();
document.getElementById('status').innerHTML = `
<strong>Current Time:</strong> ${now.toLocaleString()} |
<strong>SSID:</strong> ${'SSID_PLACEHOLDER'} |
<strong>IP:</strong> ${'IP_PLACEHOLDER'} |
<strong>CSV:</strong> ${currentFile || 'today.csv'}
`;
}
function downloadCSV() {
if (currentFile) window.location = '/file?file=' + encodeURIComponent(currentFile);
}
window.onload = () => {
document.getElementById('logDate').value = getTodayDate();
manualRefresh();
setInterval(() => {
if (document.getElementById('logDate').value === getTodayDate()) manualRefresh();
}, 30000);
setInterval(updateStatus, 1000);
updateStatus();
};
</script>
</body>
</html>
)rawliteral";
html.replace("SSID_PLACEHOLDER", ssidStr);
html.replace("IP_PLACEHOLDER", WiFi.localIP().toString());
request->send(200, "text/html", html);
});
server.on("/file", HTTP_GET, [](AsyncWebServerRequest *request) {
if (request->hasParam("file")) {
String file = request->getParam("file")->value();
if (SD.exists(file.c_str())) {
request->send(SD, file.c_str(), "text/csv");
return;
}
}
request->send(404, "text/plain", "File not found");
});
server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = "<!DOCTYPE html><html><head><title>Setup</title></head><body>";
html += "<h1>Configuration</h1><form action='/save' method='POST'>";
html += "SSID: <input name='ssid' value='" + ssidStr + "'><br><br>";
html += "Password: <input name='password' value='" + passStr + "'><br><br>";
html += "<h2>Sensor Names & Visibility</h2>";
for (int i = 0; i < NUM_SENSORS; i++) {
String ch = visible[i] ? "checked" : "";
html += "Sensor " + String(i+1) + ": <input name='name" + String(i) + "' value='" + sensorNames[i] + "'> ";
html += "<input type='checkbox' name='vis" + String(i) + "' value='1' " + ch + "> Visible<br>";
}
html += "<br><button type='submit'>Save Settings</button></form>";
html += "<br><a href='/'>← Back to Dashboard</a>";
html += "<div style='margin-top:30px;padding:15px;background:#e8f4fd;border-radius:6px;text-align:center;'>";
html += "<strong>Setup File:</strong> " + String(setupFile) + "<br>";
html += "<strong>Current Time:</strong> <span id='setupTime'></span>";
html += "</div>";
html += R"rawliteral(
<script>
function updateSetupTime() {
const now = new Date();
document.getElementById('setupTime').innerText = now.toLocaleString();
}
setInterval(updateSetupTime, 1000);
updateSetupTime();
</script>
</body></html>
)rawliteral";
request->send(200, "text/html", html);
});
server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("ssid", true)) ssidStr = request->getParam("ssid", true)->value();
if (request->hasParam("password", true)) passStr = request->getParam("password", true)->value();
for (int i = 0; i < NUM_SENSORS; i++) {
String nKey = "name" + String(i);
String vKey = "vis" + String(i);
if (request->hasParam(nKey, true)) sensorNames[i] = request->getParam(nKey, true)->value();
visible[i] = request->hasParam(vKey, true);
}
saveSetup();
request->redirect("/");
});
server.begin();
Serial.println("Web server started.");
}
void loop() {
static unsigned long lastLog = 0;
if (millis() - lastLog > 1000) {
lastLog = millis();
float values[16];
for (int i = 0; i < NUM_SENSORS; i++) {
values[i] = 20.0 + i * 0.5 + (random(-20, 21) / 10.0);
}
logData(values);
}
}