|
| 1 | +#include <ESP8266WiFi.h> |
| 2 | +#include <ESP8266WebServer.h> |
| 3 | +#include <OneWire.h> |
| 4 | +#include <DallasTemperature.h> |
| 5 | + |
| 6 | +/* |
| 7 | + SETUP INSTRUCTIONS |
| 8 | + |
| 9 | + 1) Change WiFi SSID and Password: |
| 10 | + const char* ssid = "YourSSID"; |
| 11 | + const char* password = "YourPassword"; |
| 12 | +
|
| 13 | + 2) Polling Interval (milliseconds): |
| 14 | + const unsigned long READ_INTERVAL = 10000; // 10 seconds |
| 15 | +
|
| 16 | + 3) Number of Readings (History Length): |
| 17 | + const int HISTORY_LENGTH = 360; // 1 hour at 10-second intervals |
| 18 | +*/ |
| 19 | + |
| 20 | +const char* ssid = "YourSSID"; |
| 21 | +const char* password = "YourPassword"; |
| 22 | + |
| 23 | +const int oneWireBus = 4; |
| 24 | +const int MAX_SENSORS = 8; |
| 25 | +const int HISTORY_LENGTH = 360; |
| 26 | +const unsigned long READ_INTERVAL = 10000; |
| 27 | + |
| 28 | +DeviceAddress sensorAddresses[MAX_SENSORS]; |
| 29 | +float tempHistory[MAX_SENSORS][HISTORY_LENGTH]; |
| 30 | +int historyIndex = 0; |
| 31 | +int numberOfDevices = 0; |
| 32 | +unsigned long lastReadTime = 0; |
| 33 | + |
| 34 | +OneWire oneWire(oneWireBus); |
| 35 | +DallasTemperature sensors(&oneWire); |
| 36 | +ESP8266WebServer server(80); |
| 37 | + |
| 38 | +String getAddressString(DeviceAddress deviceAddress); |
| 39 | +void handleRoot(); |
| 40 | +void handleSensorList(); |
| 41 | +void handleTemperature(); |
| 42 | +void handleHistory(); |
| 43 | +void updateHistory(); |
| 44 | + |
| 45 | +const char MAIN_page[] PROGMEM = R"=====( |
| 46 | +<!DOCTYPE html> |
| 47 | +<html lang="en"> |
| 48 | +<head> |
| 49 | + <meta charset="UTF-8"> |
| 50 | + <meta name="viewport" |
| 51 | + content="width=device-width, initial-scale=1.0"> |
| 52 | + <title>Arduino Temperature Control Library - Sensor Data Graph</title> |
| 53 | + <script src="https://cdn.jsdelivr.net/npm/[email protected]"></script> |
| 54 | + <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" |
| 55 | + rel="stylesheet"> |
| 56 | +</head> |
| 57 | +<body class="bg-gray-100 font-sans min-h-screen flex flex-col"> |
| 58 | + <div class="container mx-auto p-6 flex-grow"> |
| 59 | + <h1 class="text-2xl font-semibold text-gray-800 mb-4"> |
| 60 | + Arduino Temperature Control Library - Sensor Data |
| 61 | + </h1> |
| 62 | +
|
| 63 | + <div class="flex mb-6"> |
| 64 | + <div class="cursor-pointer px-4 py-2 bg-blue-500 text-white rounded-lg shadow |
| 65 | + hover:bg-blue-400 active:scale-95" |
| 66 | + onclick="showTab('dashboard')"> |
| 67 | + Dashboard |
| 68 | + </div> |
| 69 | + <div class="cursor-pointer px-4 py-2 bg-gray-200 rounded-lg shadow |
| 70 | + hover:bg-gray-300 active:scale-95 ml-4" |
| 71 | + onclick="showTab('api')"> |
| 72 | + API Docs |
| 73 | + </div> |
| 74 | + <div class="cursor-pointer px-4 py-2 bg-gray-200 rounded-lg shadow |
| 75 | + hover:bg-gray-300 active:scale-95 ml-4" |
| 76 | + onclick="showTab('setup')"> |
| 77 | + Setup |
| 78 | + </div> |
| 79 | + </div> |
| 80 | +
|
| 81 | + <div id="dashboard" class="tab-content"> |
| 82 | + <button class="px-6 py-2 bg-green-500 text-white rounded-lg shadow |
| 83 | + hover:bg-green-400 active:scale-95" |
| 84 | + onclick="refreshData()"> |
| 85 | + Refresh Data |
| 86 | + </button> |
| 87 | + <div id="sensors" class="mt-6"> |
| 88 | + <div class="text-gray-600">Loading sensor data...</div> |
| 89 | + </div> |
| 90 | + </div> |
| 91 | +
|
| 92 | + <div id="api" class="tab-content hidden mt-8"> |
| 93 | + <h2 class="text-xl font-semibold text-gray-800 mb-4">API</h2> |
| 94 | + <div class="bg-white p-6 rounded-lg shadow"> |
| 95 | + <div class="mb-4"> |
| 96 | + <span class="font-semibold text-blue-500">GET</span> |
| 97 | + <a href="/temperature" class="text-blue-500 hover:underline">/temperature</a> |
| 98 | + <pre class="bg-gray-100 p-4 rounded mt-2"> |
| 99 | +{ |
| 100 | + "sensors": [ |
| 101 | + { |
| 102 | + "id": 0, |
| 103 | + "address": "28FF457D1234AB12", |
| 104 | + "celsius": 23.45, |
| 105 | + "fahrenheit": 74.21 |
| 106 | + } |
| 107 | + ] |
| 108 | +} |
| 109 | + </pre> |
| 110 | + </div> |
| 111 | + <div class="mb-4"> |
| 112 | + <span class="font-semibold text-blue-500">GET</span> |
| 113 | + <a href="/sensors" class="text-blue-500 hover:underline">/sensors</a> |
| 114 | + <pre class="bg-gray-100 p-4 rounded mt-2"> |
| 115 | +{ |
| 116 | + "sensors": [ |
| 117 | + { |
| 118 | + "id": 0, |
| 119 | + "address": "28FF457D1234AB12" |
| 120 | + } |
| 121 | + ] |
| 122 | +} |
| 123 | + </pre> |
| 124 | + </div> |
| 125 | + <div> |
| 126 | + <span class="font-semibold text-blue-500">GET</span> |
| 127 | + <a href="/history" class="text-blue-500 hover:underline">/history</a> |
| 128 | + <pre class="bg-gray-100 p-4 rounded mt-2"> |
| 129 | +{ |
| 130 | + "interval_ms": 10000, |
| 131 | + "sensors": [ |
| 132 | + { |
| 133 | + "id": 0, |
| 134 | + "address": "28FF457D1234AB12", |
| 135 | + "history": [23.45, 23.50, 23.48] |
| 136 | + } |
| 137 | + ] |
| 138 | +} |
| 139 | + </pre> |
| 140 | + </div> |
| 141 | + </div> |
| 142 | + </div> |
| 143 | +
|
| 144 | + <div id="setup" class="tab-content hidden mt-8"> |
| 145 | + <h2 class="text-xl font-semibold text-gray-800 mb-4">Setup Instructions</h2> |
| 146 | + <div class="bg-white p-6 rounded-lg shadow leading-relaxed"> |
| 147 | + <p class="mb-4"> |
| 148 | + Edit the .ino code to change SSID, password, read interval, and number |
| 149 | + of stored readings (HISTORY_LENGTH). |
| 150 | + </p> |
| 151 | + </div> |
| 152 | + </div> |
| 153 | + </div> |
| 154 | +
|
| 155 | + <footer class="bg-gray-800 text-white p-4 mt-auto text-center"> |
| 156 | + <p>© 2025 Miles Burton. All Rights Reserved.</p> |
| 157 | + <p> |
| 158 | + Licensed under the |
| 159 | + <a href="https://opensource.org/licenses/MIT" class="text-blue-400 hover:underline"> |
| 160 | + MIT License |
| 161 | + </a>. |
| 162 | + </p> |
| 163 | + </footer> |
| 164 | +
|
| 165 | + <script> |
| 166 | + const showTab = (name) => { |
| 167 | + document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); |
| 168 | + document.getElementById(name).classList.remove('hidden'); |
| 169 | + }; |
| 170 | + const buildSensorsHTML = (sensors) => { |
| 171 | + return sensors.map(s => { |
| 172 | + const chartId = "chart-" + s.id; |
| 173 | + return ` |
| 174 | + <div class="bg-white p-6 rounded-lg shadow mb-6"> |
| 175 | + <div class="text-lg font-semibold text-blue-500"> |
| 176 | + ${s.celsius.toFixed(2)}°C / ${s.fahrenheit.toFixed(2)}°F |
| 177 | + </div> |
| 178 | + <div class="text-sm text-gray-600 mt-2"> |
| 179 | + Sensor ID: ${s.id} (${s.address}) |
| 180 | + </div> |
| 181 | + <div class="mt-4" style="height: 300px;"> |
| 182 | + <canvas id="${chartId}"></canvas> |
| 183 | + </div> |
| 184 | + </div> |
| 185 | + `; |
| 186 | + }).join(''); |
| 187 | + }; |
| 188 | + const drawChart = (chartId, dataPoints) => { |
| 189 | + const ctx = document.getElementById(chartId); |
| 190 | + if (!ctx) return; |
| 191 | + new Chart(ctx, { |
| 192 | + type: 'line', |
| 193 | + data: { |
| 194 | + labels: dataPoints.map(p => p.x), |
| 195 | + datasets: [{ |
| 196 | + label: 'Temp (°C)', |
| 197 | + data: dataPoints, |
| 198 | + borderColor: 'red', |
| 199 | + backgroundColor: 'rgba(255,0,0,0.1)', |
| 200 | + borderWidth: 2, |
| 201 | + pointRadius: 3, |
| 202 | + lineTension: 0.1, |
| 203 | + fill: true |
| 204 | + }] |
| 205 | + }, |
| 206 | + options: { |
| 207 | + responsive: true, |
| 208 | + maintainAspectRatio: false, |
| 209 | + plugins: { |
| 210 | + tooltip: { |
| 211 | + mode: 'index', |
| 212 | + intersect: false, |
| 213 | + callbacks: { |
| 214 | + label: (ctx) => { |
| 215 | + const t = ctx.parsed.x; |
| 216 | + const d = new Date(t); |
| 217 | + return `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(2)}°C at ${d.toLocaleTimeString()}`; |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + }, |
| 222 | + interaction: { mode: 'nearest', intersect: true }, |
| 223 | + scales: { |
| 224 | + x: { |
| 225 | + type: 'linear', |
| 226 | + position: 'bottom', |
| 227 | + ticks: { |
| 228 | + autoSkip: true, |
| 229 | + maxTicksLimit: 10, |
| 230 | + callback: (v) => new Date(v).toLocaleTimeString() |
| 231 | + } |
| 232 | + }, |
| 233 | + y: { |
| 234 | + grid: { color: 'rgba(0,0,0,0.05)' } |
| 235 | + } |
| 236 | + } |
| 237 | + } |
| 238 | + }); |
| 239 | + }; |
| 240 | + const refreshData = async () => { |
| 241 | + try { |
| 242 | + const sensorsDiv = document.getElementById('sensors'); |
| 243 | + sensorsDiv.innerHTML = '<div class="text-gray-600">Loading sensor data...</div>'; |
| 244 | + const td = await fetch('/temperature'); |
| 245 | + const tempData = await td.json(); |
| 246 | + const hd = await fetch('/history'); |
| 247 | + const historyData = await hd.json(); |
| 248 | + sensorsDiv.innerHTML = buildSensorsHTML(tempData.sensors); |
| 249 | + tempData.sensors.forEach(s => { |
| 250 | + const chartId = "chart-" + s.id; |
| 251 | + const sensorHist = historyData.sensors.find(h => h.id === s.id); |
| 252 | + if (!sensorHist) return; |
| 253 | + const total = sensorHist.history.length; |
| 254 | + const arr = sensorHist.history.map((v, i) => { |
| 255 | + return { x: Date.now() - (total - 1 - i)*10000, y: v }; |
| 256 | + }); |
| 257 | + drawChart(chartId, arr); |
| 258 | + }); |
| 259 | + } catch(e) { |
| 260 | + console.error(e); |
| 261 | + document.getElementById('sensors').innerHTML = |
| 262 | + '<div class="text-gray-600">Error loading data.</div>'; |
| 263 | + } |
| 264 | + }; |
| 265 | + refreshData(); |
| 266 | + setInterval(refreshData, 30000); |
| 267 | + </script> |
| 268 | +</body> |
| 269 | +</html> |
| 270 | +)====="; |
| 271 | + |
| 272 | +void setup() { |
| 273 | + Serial.begin(115200); |
| 274 | + sensors.begin(); |
| 275 | + |
| 276 | + for (int i = 0; i < MAX_SENSORS; i++) { |
| 277 | + for (int j = 0; j < HISTORY_LENGTH; j++) { |
| 278 | + tempHistory[i][j] = 0; |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + numberOfDevices = sensors.getDeviceCount(); |
| 283 | + if (numberOfDevices > MAX_SENSORS) { |
| 284 | + numberOfDevices = MAX_SENSORS; |
| 285 | + } |
| 286 | + |
| 287 | + for (int i = 0; i < numberOfDevices; i++) { |
| 288 | + sensors.getAddress(sensorAddresses[i], i); |
| 289 | + } |
| 290 | + |
| 291 | + sensors.setResolution(12); |
| 292 | + |
| 293 | + WiFi.begin(ssid, password); |
| 294 | + while (WiFi.status() != WL_CONNECTED) { |
| 295 | + delay(500); |
| 296 | + } |
| 297 | + |
| 298 | + server.on("/", HTTP_GET, handleRoot); |
| 299 | + server.on("/temperature", HTTP_GET, handleTemperature); |
| 300 | + server.on("/sensors", HTTP_GET, handleSensorList); |
| 301 | + server.on("/history", HTTP_GET, handleHistory); |
| 302 | + |
| 303 | + server.begin(); |
| 304 | +} |
| 305 | + |
| 306 | +void loop() { |
| 307 | + server.handleClient(); |
| 308 | + unsigned long t = millis(); |
| 309 | + if (t - lastReadTime >= READ_INTERVAL) { |
| 310 | + updateHistory(); |
| 311 | + lastReadTime = t; |
| 312 | + } |
| 313 | +} |
| 314 | + |
| 315 | +void updateHistory() { |
| 316 | + sensors.requestTemperatures(); |
| 317 | + for (int i = 0; i < numberOfDevices; i++) { |
| 318 | + float tempC = sensors.getTempC(sensorAddresses[i]); |
| 319 | + tempHistory[i][historyIndex] = tempC; |
| 320 | + } |
| 321 | + historyIndex = (historyIndex + 1) % HISTORY_LENGTH; |
| 322 | +} |
| 323 | + |
| 324 | +void handleRoot() { |
| 325 | + server.send(200, "text/html", MAIN_page); |
| 326 | +} |
| 327 | + |
| 328 | +void handleSensorList() { |
| 329 | + String json = "{\"sensors\":["; |
| 330 | + for (int i = 0; i < numberOfDevices; i++) { |
| 331 | + if (i > 0) json += ","; |
| 332 | + json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\"}"; |
| 333 | + } |
| 334 | + json += "]}"; |
| 335 | + server.send(200, "application/json", json); |
| 336 | +} |
| 337 | + |
| 338 | +void handleTemperature() { |
| 339 | + sensors.requestTemperatures(); |
| 340 | + String json = "{\"sensors\":["; |
| 341 | + for (int i = 0; i < numberOfDevices; i++) { |
| 342 | + if (i > 0) json += ","; |
| 343 | + float c = sensors.getTempC(sensorAddresses[i]); |
| 344 | + float f = sensors.toFahrenheit(c); |
| 345 | + json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\","; |
| 346 | + json += "\"celsius\":" + String(c) + ",\"fahrenheit\":" + String(f) + "}"; |
| 347 | + } |
| 348 | + json += "]}"; |
| 349 | + server.send(200, "application/json", json); |
| 350 | +} |
| 351 | + |
| 352 | +void handleHistory() { |
| 353 | + String json = "{\"interval_ms\":" + String(READ_INTERVAL) + ",\"sensors\":["; |
| 354 | + for (int i = 0; i < numberOfDevices; i++) { |
| 355 | + if (i > 0) json += ","; |
| 356 | + json += "{\"id\":" + String(i) + ",\"address\":\"" + getAddressString(sensorAddresses[i]) + "\",\"history\":["; |
| 357 | + for (int j = 0; j < HISTORY_LENGTH; j++) { |
| 358 | + int idx = (historyIndex - j + HISTORY_LENGTH) % HISTORY_LENGTH; |
| 359 | + if (j > 0) json += ","; |
| 360 | + json += String(tempHistory[i][idx]); |
| 361 | + } |
| 362 | + json += "]}"; |
| 363 | + } |
| 364 | + json += "]}"; |
| 365 | + server.send(200, "application/json", json); |
| 366 | +} |
| 367 | + |
| 368 | +String getAddressString(DeviceAddress deviceAddress) { |
| 369 | + String addr; |
| 370 | + for (uint8_t i = 0; i < 8; i++) { |
| 371 | + if (deviceAddress[i] < 16) addr += "0"; |
| 372 | + addr += String(deviceAddress[i], HEX); |
| 373 | + } |
| 374 | + return addr; |
| 375 | +} |
0 commit comments