|
11 | 11 | * - Time-based pump control for better low-end performance. |
12 | 12 | * - Web UI to set target temperature, PID tunings, and pump mode (Off/Auto/Manual). |
13 | 13 | * - Web server runs in a dedicated FreeRTOS task for improved responsiveness. |
| 14 | + * - Efficient web updates: full history on load, single-point updates thereafter. |
14 | 15 | * - Reads from DS18B20 temperature sensors and an HX711 pressure sensor. |
15 | 16 | * - Compensates for known pressure sensor drift. |
16 | 17 | * - Stores sensor data in memory. |
@@ -132,6 +133,7 @@ void handlePumpModeControl(); |
132 | 133 | void handleGetPumpMode(); |
133 | 134 | void handleManualPumpControl(); |
134 | 135 | void handleDataJson(); |
| 136 | +void handleDataUpdate(); |
135 | 137 | void handleDownloadCsv(); |
136 | 138 | void handleNotFound(); |
137 | 139 | String getSensorAddressString(DeviceAddress deviceAddress); |
@@ -426,6 +428,7 @@ void setupWebServer() { |
426 | 428 | server.on("/pump/mode/get", HTTP_GET, handleGetPumpMode); |
427 | 429 | server.on("/pump/manual", HTTP_POST, handleManualPumpControl); |
428 | 430 | server.on("/data.json", HTTP_GET, handleDataJson); |
| 431 | + server.on("/data/update", HTTP_GET, handleDataUpdate); |
429 | 432 | server.on("/download.csv", HTTP_GET, handleDownloadCsv); |
430 | 433 | server.onNotFound(handleNotFound); |
431 | 434 |
|
@@ -546,93 +549,106 @@ void handleRoot() { |
546 | 549 | </div> |
547 | 550 | <script> |
548 | 551 | let myChart; // Variable to hold the chart instance |
| 552 | + const MAX_CHART_POINTS = 3000; |
549 | 553 |
|
550 | | - const fetchData = () => { |
551 | | - return fetch('/data.json').then(response => response.json()); |
| 554 | + const initialChartLoad = () => { |
| 555 | + fetch('/data.json') |
| 556 | + .then(response => response.json()) |
| 557 | + .then(data => createChart(data)) |
| 558 | + .catch(error => console.error('Initial chart load error:', error)); |
552 | 559 | }; |
553 | 560 |
|
554 | | - const createOrUpdateChart = () => { |
555 | | - fetchData().then(data => { |
556 | | - const ctx = document.getElementById('tempChart').getContext('2d'); |
557 | | - |
558 | | - const labels = data.map(d => { |
559 | | - const date = new Date(d.time * 1000); |
560 | | - return date.toLocaleTimeString(); |
561 | | - }); |
| 561 | + const updateChart = () => { |
| 562 | + fetch('/data/update') |
| 563 | + .then(response => response.json()) |
| 564 | + .then(point => { |
| 565 | + if (!myChart || !point.time) return; // Don't update if chart not ready or no new data |
| 566 | +
|
| 567 | + const newLabel = new Date(point.time * 1000).toLocaleTimeString(); |
| 568 | + myChart.data.labels.push(newLabel); |
| 569 | + myChart.data.datasets[0].data.push(point.temp1); |
| 570 | + myChart.data.datasets[1].data.push(point.temp2); |
| 571 | + myChart.data.datasets[2].data.push(point.pumpPower); |
| 572 | + myChart.data.datasets[3].data.push(point.pressure); |
| 573 | +
|
| 574 | + // Remove oldest data point if we're over the max |
| 575 | + if (myChart.data.labels.length > MAX_CHART_POINTS) { |
| 576 | + myChart.data.labels.shift(); |
| 577 | + myChart.data.datasets.forEach((dataset) => { |
| 578 | + dataset.data.shift(); |
| 579 | + }); |
| 580 | + } |
562 | 581 |
|
563 | | - if (myChart) { |
564 | | - // If chart exists, update data and redraw |
565 | | - myChart.data.labels = labels; |
566 | | - myChart.data.datasets[0].data = data.map(d => d.temp1); |
567 | | - myChart.data.datasets[1].data = data.map(d => d.temp2); |
568 | | - myChart.data.datasets[2].data = data.map(d => d.pumpPower); |
569 | | - myChart.data.datasets[3].data = data.map(d => d.pressure); |
570 | | - myChart.update(); |
571 | | - } else { |
572 | | - // If chart doesn't exist, create it |
573 | | - myChart = new Chart(ctx, { |
574 | | - type: 'line', |
575 | | - data: { |
576 | | - labels: labels, |
577 | | - datasets: [{ |
578 | | - label: 'Sensor 1 (°C)', |
579 | | - data: data.map(d => d.temp1), |
580 | | - borderColor: 'rgba(255, 99, 132, 1)', |
581 | | - yAxisID: 'y-temp', |
582 | | - fill: false |
583 | | - }, { |
584 | | - label: 'Sensor 2 (°C)', |
585 | | - data: data.map(d => d.temp2), |
586 | | - borderColor: 'rgba(54, 162, 235, 1)', |
587 | | - yAxisID: 'y-temp', |
588 | | - fill: false |
589 | | - }, { |
590 | | - label: 'Pump Power (%)', |
591 | | - data: data.map(d => d.pumpPower), |
592 | | - borderColor: 'rgba(75, 192, 192, 1)', |
593 | | - backgroundColor: 'rgba(75, 192, 192, 0.2)', |
594 | | - yAxisID: 'y-power', |
595 | | - fill: true |
596 | | - }, { |
597 | | - label: 'Pressure', |
598 | | - data: data.map(d => d.pressure), |
599 | | - borderColor: 'rgba(255, 159, 64, 1)', |
600 | | - yAxisID: 'y-pressure', |
601 | | - fill: false |
602 | | - }] |
| 582 | + myChart.update(); |
| 583 | + }) |
| 584 | + .catch(error => console.error('Chart update error:', error)); |
| 585 | + }; |
| 586 | +
|
| 587 | + const createChart = (data) => { |
| 588 | + const ctx = document.getElementById('tempChart').getContext('2d'); |
| 589 | + const labels = data.map(d => new Date(d.time * 1000).toLocaleTimeString()); |
| 590 | + |
| 591 | + myChart = new Chart(ctx, { |
| 592 | + type: 'line', |
| 593 | + data: { |
| 594 | + labels: labels, |
| 595 | + datasets: [{ |
| 596 | + label: 'Sensor 1 (°C)', |
| 597 | + data: data.map(d => d.temp1), |
| 598 | + borderColor: 'rgba(255, 99, 132, 1)', |
| 599 | + yAxisID: 'y-temp', |
| 600 | + fill: false |
| 601 | + }, { |
| 602 | + label: 'Sensor 2 (°C)', |
| 603 | + data: data.map(d => d.temp2), |
| 604 | + borderColor: 'rgba(54, 162, 235, 1)', |
| 605 | + yAxisID: 'y-temp', |
| 606 | + fill: false |
| 607 | + }, { |
| 608 | + label: 'Pump Power (%)', |
| 609 | + data: data.map(d => d.pumpPower), |
| 610 | + borderColor: 'rgba(75, 192, 192, 1)', |
| 611 | + backgroundColor: 'rgba(75, 192, 192, 0.2)', |
| 612 | + yAxisID: 'y-power', |
| 613 | + fill: true |
| 614 | + }, { |
| 615 | + label: 'Pressure', |
| 616 | + data: data.map(d => d.pressure), |
| 617 | + borderColor: 'rgba(255, 159, 64, 1)', |
| 618 | + yAxisID: 'y-pressure', |
| 619 | + fill: false |
| 620 | + }] |
| 621 | + }, |
| 622 | + options: { |
| 623 | + responsive: true, |
| 624 | + maintainAspectRatio: false, |
| 625 | + scales: { |
| 626 | + x: { display: true, title: { display: true, text: 'Time' } }, |
| 627 | + 'y-temp': { |
| 628 | + type: 'linear', |
| 629 | + display: true, |
| 630 | + position: 'left', |
| 631 | + title: { display: true, text: 'Temperature (°C)' } |
603 | 632 | }, |
604 | | - options: { |
605 | | - responsive: true, |
606 | | - maintainAspectRatio: false, |
607 | | - scales: { |
608 | | - x: { display: true, title: { display: true, text: 'Time' } }, |
609 | | - 'y-temp': { |
610 | | - type: 'linear', |
611 | | - display: true, |
612 | | - position: 'left', |
613 | | - title: { display: true, text: 'Temperature (°C)' } |
614 | | - }, |
615 | | - 'y-power': { |
616 | | - type: 'linear', |
617 | | - display: true, |
618 | | - position: 'right', |
619 | | - min: 0, |
620 | | - max: 100, |
621 | | - title: { display: true, text: 'Pump Power (%)' }, |
622 | | - grid: { drawOnChartArea: false } |
623 | | - }, |
624 | | - 'y-pressure': { |
625 | | - type: 'linear', |
626 | | - display: true, |
627 | | - position: 'right', |
628 | | - title: { display: true, text: 'Pressure' }, |
629 | | - grid: { drawOnChartArea: false } |
630 | | - } |
631 | | - } |
| 633 | + 'y-power': { |
| 634 | + type: 'linear', |
| 635 | + display: true, |
| 636 | + position: 'right', |
| 637 | + min: 0, |
| 638 | + max: 100, |
| 639 | + title: { display: true, text: 'Pump Power (%)' }, |
| 640 | + grid: { drawOnChartArea: false } |
| 641 | + }, |
| 642 | + 'y-pressure': { |
| 643 | + type: 'linear', |
| 644 | + display: true, |
| 645 | + position: 'right', |
| 646 | + title: { display: true, text: 'Pressure' }, |
| 647 | + grid: { drawOnChartArea: false } |
632 | 648 | } |
633 | | - }); |
| 649 | + } |
634 | 650 | } |
635 | | - }).catch(error => console.error('Chart update error:', error)); |
| 651 | + }); |
636 | 652 | }; |
637 | 653 |
|
638 | 654 | // --- Control Logic --- |
@@ -711,8 +727,8 @@ void handleRoot() { |
711 | 727 | fetchAndUpdatePidInputs(); |
712 | 728 | fetchAndUpdatePumpMode(); |
713 | 729 | setpointInput.value = 78.2; // Set default temp target on page load |
714 | | - createOrUpdateChart(); |
715 | | - setInterval(createOrUpdateChart, 5000); |
| 730 | + initialChartLoad(); |
| 731 | + setInterval(updateChart, 5000); |
716 | 732 | }); |
717 | 733 | </script> |
718 | 734 | </body> |
@@ -846,6 +862,24 @@ void handleDataJson() { |
846 | 862 | server.sendContent(""); // End of stream |
847 | 863 | } |
848 | 864 |
|
| 865 | +/** |
| 866 | + * @brief Serves the latest data point as a JSON object. |
| 867 | + */ |
| 868 | +void handleDataUpdate() { |
| 869 | + if (readingCount > 0) { |
| 870 | + String json_item = "{"; |
| 871 | + json_item += "\"time\":" + String(data[readingCount - 1].time); |
| 872 | + json_item += ",\"temp1\":" + String(data[readingCount - 1].temp1); |
| 873 | + json_item += ",\"temp2\":" + String(data[readingCount - 1].temp2); |
| 874 | + json_item += ",\"pumpPower\":" + String(data[readingCount - 1].pumpPower); |
| 875 | + json_item += ",\"pressure\":" + String(data[readingCount - 1].pressure); |
| 876 | + json_item += "}"; |
| 877 | + server.send(200, "application/json", json_item); |
| 878 | + } else { |
| 879 | + server.send(200, "application/json", "{}"); // Send empty object if no data yet |
| 880 | + } |
| 881 | +} |
| 882 | + |
849 | 883 | /** |
850 | 884 | * @brief Handles the request to download data as a CSV file by streaming it. |
851 | 885 | */ |
|
0 commit comments