376 lines
11 KiB
C++
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>© 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;
|
|
}
|