2025-05-14 23:17:02 +01:00

376 lines
11 KiB
C++

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <OneWire.h>
#include <DallasTemperature.h>
/*
SETUP INSTRUCTIONS
1) Change WiFi SSID and Password:
const char* ssid = "YourSSID";
const char* password = "YourPassword";
2) Polling Interval (milliseconds):
const unsigned long READ_INTERVAL = 10000; // 10 seconds
3) Number of Readings (History Length):
const int HISTORY_LENGTH = 360; // 1 hour at 10-second intervals
*/
const char* ssid = "YourSSID";
const char* password = "YourPassword";
const int oneWireBus = 4;
const int MAX_SENSORS = 8;
const int HISTORY_LENGTH = 360;
const unsigned long READ_INTERVAL = 10000;
DeviceAddress sensorAddresses[MAX_SENSORS];
float tempHistory[MAX_SENSORS][HISTORY_LENGTH];
int historyIndex = 0;
int numberOfDevices = 0;
unsigned long lastReadTime = 0;
OneWire oneWire(oneWireBus);
DallasTemperature sensors(&oneWire);
ESP8266WebServer server(80);
String getAddressString(DeviceAddress deviceAddress);
void handleRoot();
void handleSensorList();
void handleTemperature();
void handleHistory();
void updateHistory();
const char MAIN_page[] PROGMEM = R"=====(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<title>Arduino Temperature Control Library - Sensor Data Graph</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css"
rel="stylesheet">
</head>
<body class="bg-gray-100 font-sans min-h-screen flex flex-col">
<div class="container mx-auto p-6 flex-grow">
<h1 class="text-2xl font-semibold text-gray-800 mb-4">
Arduino Temperature Control Library - Sensor Data
</h1>
<div class="flex mb-6">
<div class="cursor-pointer px-4 py-2 bg-blue-500 text-white rounded-lg shadow
hover:bg-blue-400 active:scale-95"
onclick="showTab('dashboard')">
Dashboard
</div>
<div class="cursor-pointer px-4 py-2 bg-gray-200 rounded-lg shadow
hover:bg-gray-300 active:scale-95 ml-4"
onclick="showTab('api')">
API Docs
</div>
<div class="cursor-pointer px-4 py-2 bg-gray-200 rounded-lg shadow
hover:bg-gray-300 active:scale-95 ml-4"
onclick="showTab('setup')">
Setup
</div>
</div>
<div id="dashboard" class="tab-content">
<button class="px-6 py-2 bg-green-500 text-white rounded-lg shadow
hover:bg-green-400 active:scale-95"
onclick="refreshData()">
Refresh Data
</button>
<div id="sensors" class="mt-6">
<div class="text-gray-600">Loading sensor data...</div>
</div>
</div>
<div id="api" class="tab-content hidden mt-8">
<h2 class="text-xl font-semibold text-gray-800 mb-4">API</h2>
<div class="bg-white p-6 rounded-lg shadow">
<div class="mb-4">
<span class="font-semibold text-blue-500">GET</span>
<a href="/temperature" class="text-blue-500 hover:underline">/temperature</a>
<pre class="bg-gray-100 p-4 rounded mt-2">
{
"sensors": [
{
"id": 0,
"address": "28FF457D1234AB12",
"celsius": 23.45,
"fahrenheit": 74.21
}
]
}
</pre>
</div>
<div class="mb-4">
<span class="font-semibold text-blue-500">GET</span>
<a href="/sensors" class="text-blue-500 hover:underline">/sensors</a>
<pre class="bg-gray-100 p-4 rounded mt-2">
{
"sensors": [
{
"id": 0,
"address": "28FF457D1234AB12"
}
]
}
</pre>
</div>
<div>
<span class="font-semibold text-blue-500">GET</span>
<a href="/history" class="text-blue-500 hover:underline">/history</a>
<pre class="bg-gray-100 p-4 rounded mt-2">
{
"interval_ms": 10000,
"sensors": [
{
"id": 0,
"address": "28FF457D1234AB12",
"history": [23.45, 23.50, 23.48]
}
]
}
</pre>
</div>
</div>
</div>
<div id="setup" class="tab-content hidden mt-8">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Setup Instructions</h2>
<div class="bg-white p-6 rounded-lg shadow leading-relaxed">
<p class="mb-4">
Edit the .ino code to change SSID, password, read interval, and number
of stored readings (HISTORY_LENGTH).
</p>
</div>
</div>
</div>
<footer class="bg-gray-800 text-white p-4 mt-auto text-center">
<p>&copy; 2025 Miles Burton. All Rights Reserved.</p>
<p>
Licensed under the
<a href="https://opensource.org/licenses/MIT" class="text-blue-400 hover:underline">
MIT License
</a>.
</p>
</footer>
<script>
const showTab = (name) => {
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
document.getElementById(name).classList.remove('hidden');
};
const buildSensorsHTML = (sensors) => {
return sensors.map(s => {
const chartId = "chart-" + s.id;
return `
<div class="bg-white p-6 rounded-lg shadow mb-6">
<div class="text-lg font-semibold text-blue-500">
${s.celsius.toFixed(2)}°C / ${s.fahrenheit.toFixed(2)}°F
</div>
<div class="text-sm text-gray-600 mt-2">
Sensor ID: ${s.id} (${s.address})
</div>
<div class="mt-4" style="height: 300px;">
<canvas id="${chartId}"></canvas>
</div>
</div>
`;
}).join('');
};
const drawChart = (chartId, dataPoints) => {
const ctx = document.getElementById(chartId);
if (!ctx) return;
new Chart(ctx, {
type: 'line',
data: {
labels: dataPoints.map(p => p.x),
datasets: [{
label: 'Temp (°C)',
data: dataPoints,
borderColor: 'red',
backgroundColor: 'rgba(255,0,0,0.1)',
borderWidth: 2,
pointRadius: 3,
lineTension: 0.1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: (ctx) => {
const t = ctx.parsed.x;
const d = new Date(t);
return `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(2)}°C at ${d.toLocaleTimeString()}`;
}
}
}
},
interaction: { mode: 'nearest', intersect: true },
scales: {
x: {
type: 'linear',
position: 'bottom',
ticks: {
autoSkip: true,
maxTicksLimit: 10,
callback: (v) => new Date(v).toLocaleTimeString()
}
},
y: {
grid: { color: 'rgba(0,0,0,0.05)' }
}
}
}
});
};
const refreshData = async () => {
try {
const sensorsDiv = document.getElementById('sensors');
sensorsDiv.innerHTML = '<div class="text-gray-600">Loading sensor data...</div>';
const td = await fetch('/temperature');
const tempData = await td.json();
const hd = await fetch('/history');
const historyData = await hd.json();
sensorsDiv.innerHTML = buildSensorsHTML(tempData.sensors);
tempData.sensors.forEach(s => {
const chartId = "chart-" + s.id;
const sensorHist = historyData.sensors.find(h => h.id === s.id);
if (!sensorHist) return;
const total = sensorHist.history.length;
const arr = sensorHist.history.map((v, i) => {
return { x: Date.now() - (total - 1 - i)*10000, y: v };
});
drawChart(chartId, arr);
});
} catch(e) {
console.error(e);
document.getElementById('sensors').innerHTML =
'<div class="text-gray-600">Error loading data.</div>';
}
};
refreshData();
setInterval(refreshData, 30000);
</script>
</body>
</html>
)=====";
void setup() {
Serial.begin(115200);
sensors.begin();
for (int i = 0; i < MAX_SENSORS; i++) {
for (int j = 0; j < HISTORY_LENGTH; j++) {
tempHistory[i][j] = 0;
}
}
numberOfDevices = sensors.getDeviceCount();
if (numberOfDevices > MAX_SENSORS) {
numberOfDevices = MAX_SENSORS;
}
for (int i = 0; i < numberOfDevices; i++) {
sensors.getAddress(sensorAddresses[i], i);
}
sensors.setResolution(12);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
server.on("/", HTTP_GET, handleRoot);
server.on("/temperature", HTTP_GET, handleTemperature);
server.on("/sensors", HTTP_GET, handleSensorList);
server.on("/history", HTTP_GET, handleHistory);
server.begin();
}
void loop() {
server.handleClient();
unsigned long t = millis();
if (t - lastReadTime >= READ_INTERVAL) {
updateHistory();
lastReadTime = t;
}
}
void updateHistory() {
sensors.requestTemperatures();
for (int i = 0; i < numberOfDevices; i++) {
float tempC = sensors.getTempC(sensorAddresses[i]);
tempHistory[i][historyIndex] = tempC;
}
historyIndex = (historyIndex + 1) % HISTORY_LENGTH;
}
void handleRoot() {
server.send(200, "text/html", MAIN_page);
}
void handleSensorList() {
String json = "{\"sensors\":[";
for (int i = 0; i < numberOfDevices; i++) {
if (i > 0) json += ",";
json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\"}";
}
json += "]}";
server.send(200, "application/json", json);
}
void handleTemperature() {
sensors.requestTemperatures();
String json = "{\"sensors\":[";
for (int i = 0; i < numberOfDevices; i++) {
if (i > 0) json += ",";
float c = sensors.getTempC(sensorAddresses[i]);
float f = sensors.toFahrenheit(c);
json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\",";
json += "\"celsius\":" + String(c) + ",\"fahrenheit\":" + String(f) + "}";
}
json += "]}";
server.send(200, "application/json", json);
}
void handleHistory() {
String json = "{\"interval_ms\":" + String(READ_INTERVAL) + ",\"sensors\":[";
for (int i = 0; i < numberOfDevices; i++) {
if (i > 0) json += ",";
json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\",\"history\":[";
for (int j = 0; j < HISTORY_LENGTH; j++) {
int idx = (historyIndex - j + HISTORY_LENGTH) % HISTORY_LENGTH;
if (j > 0) json += ",";
json += String(tempHistory[i][idx]);
}
json += "]}";
}
json += "]}";
server.send(200, "application/json", json);
}
String getAddressString(DeviceAddress deviceAddress) {
String addr;
for (uint8_t i = 0; i < 8; i++) {
if (deviceAddress[i] < 16) addr += "0";
addr += String(deviceAddress[i], HEX);
}
return addr;
}