// Edit from https://randomnerdtutorials.com/esp32-plot-readings-charts-multiple/
// https://chatgpt.com/share/69af1926-88f8-8006-bf28-2463ff38d7a2
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <SD.h>
#include <time.h>
const char* ssid = "Wokwi-GUEST";
const char* password = "";
WebServer server(80);
#define SD_CS 32
const int analogPins[5] = {34, 35, 33, 25, 26};
String logfile = "/data.csv";
unsigned long lastLog = 0;
int logInterval = 5000; // 5000 = 0.5sec
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -21600;
const int daylightOffset_sec = 3600;
//----------------------------------------------------------------------
// Get Timestamp
//----------------------------------------------------------------------
String getTimestamp(){
struct tm timeinfo;
if (!getLocalTime(&timeinfo))
return "0000-00-00 00:00:00";
char buffer[25];
strftime(buffer, 25, "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(buffer);
}
//----------------------------------------------------------------------
// Log Data
//----------------------------------------------------------------------
void logData(){
File file = SD.open(logfile, FILE_APPEND);
if (!file) return;
String line = getTimestamp();
for (int i = 0; i < 5; i++) {
int value = analogRead(analogPins[i]);
line += ",";
line += String(value);
}
//Serial.println(line);
file.println(line);
file.close();
}
//----------------------------------------------------------------------
// Time Stamp Range
//----------------------------------------------------------------------
bool timestampInRange(String ts, String start, String end){
if (start == "" || end == "") return true;
if (ts >= start && ts <= end) return true;
return false;
}
//----------------------------------------------------------------------
// Read Filtered CSV
//----------------------------------------------------------------------
String readFilteredCSV(String start, String end){
File file = SD.open(logfile);
if (!file) return "";
String json = "{";
json += "\"time\":[],\"p34\":[],\"p35\":[],\"p33\":[],\"p25\":[],\"p26\":[]";
json += "}";
String out = "";
String timeArray = "";
String p34 = "";
String p35 = "";
String p33 = "";
String p25 = "";
String p26 = "";
while (file.available()){
String line = file.readStringUntil('\n');
if (line.length() < 10) continue;
int idx1 = line.indexOf(',');
String ts = line.substring(0, idx1);
if (!timestampInRange(ts, start, end)) continue;
int values[5];
int last = idx1 + 1;
for (int i = 0; i < 5; i++) {
int next = line.indexOf(',', last);
if (next == -1) next = line.length();
values[i] = line.substring(last, next).toInt();
last = next + 1;
}
timeArray += "\"" + ts + "\",";
p34 += String(values[0]) + ",";
p35 += String(values[1]) + ",";
p33 += String(values[2]) + ",";
p25 += String(values[3]) + ",";
p26 += String(values[4]) + ",";
}
file.close();
if (timeArray.endsWith(",")) timeArray.remove(timeArray.length() - 1);
if (p34.endsWith(",")) p34.remove(p34.length() - 1);
if (p35.endsWith(",")) p35.remove(p35.length() - 1);
if (p33.endsWith(",")) p33.remove(p33.length() - 1);
if (p25.endsWith(",")) p25.remove(p25.length() - 1);
if (p26.endsWith(",")) p26.remove(p26.length() - 1);
out = "{";
out += "\"time\":[" + timeArray + "],";
out += "\"p34\":[" + p34 + "],";
out += "\"p35\":[" + p35 + "],";
out += "\"p33\":[" + p33 + "],";
out += "\"p25\":[" + p25 + "],";
out += "\"p26\":[" + p26 + "]";
out += "}";
return out;
}
//----------------------------------------------------------------------
// Handle Data
//----------------------------------------------------------------------
void handleData(){
String start = server.arg("start");
String end = server.arg("end");
start.replace("T", " ");
end.replace("T", " ");
String json = readFilteredCSV(start, end);
server.send(200, "application/json", json);
}
//----------------------------------------------------------------------
// ROOT
//----------------------------------------------------------------------
void handleRoot()
{
String page = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Plant Logger</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h2>Plant Moisture Log</h2>
Start:
<input type="datetime-local" id="start">
End:
<input type="datetime-local" id="end">
<button onclick="loadData()">Load Chart</button>
<br><br>
<canvas id="chart" width="1200" height="400"></canvas>
<script>
let chart;
function loadData() {
let start = document.getElementById("start").value;
let end = document.getElementById("end").value;
fetch(`/data?start=${start}&end=${end}`)
.then(response => response.json())
.then(data => {
let ctx = document.getElementById('chart').getContext('2d');
if (chart) chart.destroy();
chart = new Chart(ctx, {
type: 'line',
data: {
labels: data.time,
datasets: [
{ label: 'GPIO34', data: data.p34 },
{ label: 'GPIO35', data: data.p35 },
{ label: 'GPIO33', data: data.p33 },
{ label: 'GPIO25', data: data.p25 },
{ label: 'GPIO26', data: data.p26 }
]
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false
},
scales: {
x: { display: true },
y: { display: true }
}
}
});
});
}
</script>
</body>
</html>
)rawliteral";
server.send(200,"text/html",page);
}
//----------------------------------------------------------------------
// SETUP
//----------------------------------------------------------------------
void setup()
{
Serial.begin(115200);
WiFi.begin(ssid,password,6);
while(WiFi.status()!=WL_CONNECTED) {
delay(500);
}
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if(!SD.begin(SD_CS)) {
Serial.println("SD failed");
return;
}
if(!SD.exists(logfile)) {
File file = SD.open(logfile, FILE_WRITE);
file.println("timestamp,p34,p35,p33,p25,p26");
file.close();
}
server.on("/",handleRoot);
server.on("/data",handleData);
server.begin();
}
//----------------------------------------------------------------------
// LOOP
//----------------------------------------------------------------------
void loop(){
server.handleClient();
if(millis()-lastLog > logInterval) {
lastLog = millis();
logData();
}
}