mirror of
				https://github.com/esp8266/Arduino.git
				synced 2025-10-30 04:26:50 +03:00 
			
		
		
		
	* New Graph Example * Now using isFlashInterfacePin() no define default GPIO mask. * Added info about zooming. * Adressed requested changes (boolean > bool, using esp8266::polledTimeout::periodicMs, reducing complexity)
		
			
				
	
	
		
			528 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			528 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <html>
 | |
| <head>
 | |
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js"></script>
 | |
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.js"></script>
 | |
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/0.7.7/chartjs-plugin-zoom.js"></script>
 | |
|   
 | |
|   <!-- or instead, put those files on the ESP filesystem along with index.htm and use:
 | |
|   <script src="Chart.js"></script>
 | |
|   <script src="hammer.js"></script>
 | |
|   <script src="chartjs-plugin-zoom.js"></script>
 | |
|   -->
 | |
|   <style>
 | |
|     body {
 | |
|       height:100%; 
 | |
|       padding:0; 
 | |
|       margin:0; 
 | |
|       width:100%;
 | |
|       display:flex; 
 | |
|       flex-direction:column;
 | |
|     }
 | |
|     #config {
 | |
|       margin:0px 8px;
 | |
|       display: flex;
 | |
|       flex-flow: row;
 | |
|       align-items: center;
 | |
|       justify-content: flex-start;
 | |
|       height: 40px;
 | |
|     }
 | |
|     #periodRange {
 | |
|       width:100%;
 | |
|     }
 | |
|     .left {
 | |
|       float:left;
 | |
|     }
 | |
|     #charts {
 | |
|       flex-grow:1;
 | |
|     }
 | |
|   </style>
 | |
| </head>
 | |
| <body >
 | |
|   <div id="config">
 | |
|     <span class="left">Max number of samples: </span>
 | |
|     <input id="maxSamplesField" type="number" min="100" max="10000" value="1000" step="100" size="4" class="left" />    
 | |
|     <span class="left">. Sampling period: </span>
 | |
|     <input id="periodField" type="number" min="0" max="10000" value="1000" step="100" size="4" class="left" />
 | |
|     <span class="left"> ms </span>
 | |
|     <input id="periodRange" type="range"  min="0" max="10000" value="1000" step="100" />
 | |
|     <button id="startPauseButton">Pause</button>
 | |
|     <button id="clearButton" disabled="true">Clear</button>
 | |
|   </div>
 | |
|   <div id="charts">
 | |
|     <div class="chart-container" style="position: relative; height:50%;"><canvas id="digital" style="background-color:#000000"></canvas></div>
 | |
|     <div class="chart-container" style="position: relative; height:25%;"><canvas id="analog"></canvas></div>
 | |
|     <div class="chart-container" style="position: relative; height:25%;"><canvas id="heap" style="background-color:#EEEEEE"></canvas>
 | |
|   </div>
 | |
|   <script>  
 | |
| 
 | |
|   /////////////////////////////////////
 | |
|   // CREATE DIGITAL CHART
 | |
|   
 | |
|   // Remember all GPIO datasets
 | |
|   var allDigitalDatasets = [];  
 | |
| 
 | |
|   var gpioColors = ['#D2FF71', '#59FFF4', '#DB399C', '#4245FF', '#FFB312', '#BB54E9', '#0BD400', '#42C2FF', '#FFFFFF', '#FF4242'];
 | |
|  
 | |
|   var digitalChart = new Chart(document.getElementById('digital'), {
 | |
|     // The type of chart we want to create
 | |
|     type: 'scatter',      
 | |
| 
 | |
|     // The data for our dataset
 | |
|     data: {
 | |
|       // 17 GPIO datasets will be added by a loop at init time
 | |
|       datasets: []
 | |
|     },
 | |
| 
 | |
|     // Configuration options go here
 | |
|     options: {
 | |
|       maintainAspectRatio: false,
 | |
|       title: {
 | |
|         display: true,
 | |
|         text: 'Digital I/Os'
 | |
|       },
 | |
|       scales: {
 | |
|         xAxes: [{
 | |
|           id:"x-axis",
 | |
|           ticks: {
 | |
|             minRotation:0,
 | |
|             maxRotation:0
 | |
|           }
 | |
|         }],
 | |
|         // 17 GPIO scales will be added by a loop at init time
 | |
|         yAxes: []
 | |
|       },
 | |
|       tooltips: {
 | |
|         intersect: false
 | |
|       },
 | |
|       animation: {
 | |
|           duration: 0
 | |
|       },
 | |
|       hover: {
 | |
|           animationDuration: 0
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| 
 | |
| 
 | |
|   /////////////////////////////////////
 | |
|   // CREATE ANALOG CHART
 | |
|   
 | |
|   var analogChart = new Chart(document.getElementById('analog'), {
 | |
|     // The type of chart we want to create
 | |
|     type: 'scatter',
 | |
| 
 | |
|     // The data for our dataset
 | |
|     data: {
 | |
|       datasets: [{
 | |
|         label: 'Analog',
 | |
|         backgroundColor: 'rgb(0,0,0,0)',
 | |
|         borderColor: 'rgb(255, 99, 132)',
 | |
|         data: [],
 | |
|         showLine:true
 | |
|       }]
 | |
|     },
 | |
| 
 | |
|     // Configuration options go here
 | |
|     options: {
 | |
|       maintainAspectRatio: false,
 | |
|       title: {
 | |
|         display: true,
 | |
|         text: 'Analog input'
 | |
|       },
 | |
|       legend: {
 | |
|         display: false
 | |
|       },
 | |
|       scales: {
 | |
|         xAxes: [{
 | |
|           id:"x-axis",
 | |
|           ticks: {
 | |
|             minRotation:0,
 | |
|             maxRotation:0
 | |
|           }
 | |
|         }],
 | |
|         yAxes: [{
 | |
|           ticks: {
 | |
|             beginAtZero:true
 | |
|           }
 | |
|         }]
 | |
|       },
 | |
|       tooltips: {
 | |
|         intersect: false
 | |
|       },
 | |
|       animation: {
 | |
|         duration: 0
 | |
|       },
 | |
|       hover: {
 | |
|         animationDuration: 0
 | |
|       },
 | |
|       
 | |
|       // Zoom plugin options
 | |
|       plugins: {
 | |
|         zoom: {
 | |
|           pan: {
 | |
|             enabled: true,
 | |
|             mode: 'y',
 | |
|           },
 | |
|           zoom: {
 | |
|             enabled: true,
 | |
|             mode: 'y',
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|       
 | |
|       onClick: function() {this.resetZoom()}
 | |
|     }
 | |
| 
 | |
|   });
 | |
| 
 | |
|   
 | |
|   /////////////////////////////////////
 | |
|   // CREATE HEAP CHART
 | |
|   
 | |
|   var heapChart = new Chart(document.getElementById('heap'), {
 | |
|     // The type of chart we want to create
 | |
|     type: 'scatter',
 | |
| 
 | |
|     // The data for our dataset
 | |
|     data: {
 | |
|       datasets: [{
 | |
|         label: 'Heap',
 | |
|         backgroundColor: 'rgb(99, 99, 255)',
 | |
|         borderColor: 'rgb(99, 99, 255)',
 | |
|         data: [],
 | |
|         showLine:true,
 | |
|         cubicInterpolationMode:'monotone'
 | |
|       }]
 | |
|     },
 | |
| 
 | |
|     // Configuration options go here
 | |
|     options: {
 | |
|       maintainAspectRatio: false,
 | |
|       title: {
 | |
|         display: true,
 | |
|         text: 'Free heap'
 | |
|       },
 | |
|       legend: {
 | |
|         display: false
 | |
|       },
 | |
|       scales: {
 | |
|         xAxes: [{
 | |
|           id:"x-axis",
 | |
|           ticks: {
 | |
|             minRotation:0,
 | |
|             maxRotation:0
 | |
|           }
 | |
|         }],
 | |
|         yAxes: [{
 | |
|           ticks: {
 | |
|             beginAtZero:true
 | |
|           }
 | |
|         }]          
 | |
|       },
 | |
|       tooltips: {
 | |
|         intersect: false
 | |
|       },
 | |
|       animation: {
 | |
|           duration: 0
 | |
|       },
 | |
|       hover: {
 | |
|           animationDuration: 0
 | |
|       },
 | |
|       
 | |
|       // Zoom plugin options
 | |
|       plugins: {
 | |
|         zoom: {
 | |
|           pan: {
 | |
|             enabled: true,
 | |
|             mode: 'y',
 | |
|           },
 | |
|           zoom: {
 | |
|             enabled: true,
 | |
|             mode: 'y',
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|       
 | |
|       onClick: function() {this.resetZoom()}
 | |
| 
 | |
|     }
 | |
|   });
 | |
| 
 | |
| 
 | |
|   /////////////////////////////////////
 | |
|   // UTILITY FUNCTIONS
 | |
| 
 | |
|   // Crete a blank dataset for the Digital chart
 | |
|   function createDigitalDataset(gpio) {  
 | |
|     return {
 | |
|       showLine:true,
 | |
|       steppedLine: true,
 | |
|       backgroundColor: 'rgb(0, 0, 0, 0)',
 | |
|       xAxisID: 'x-axis',
 | |
|       pointRadius: 1,
 | |
|       borderWidth: 1,
 | |
| 
 | |
|       data: [],
 | |
|       label: gpio,
 | |
|       yAxisID: 'gpio' + gpio + '-axis'
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   
 | |
|   // Init structures for digitalChart
 | |
|   function initDigitalChart() {
 | |
|     // Clear auto-generated Y axis
 | |
|     digitalChart.options.scales.yAxes.length = 0;
 | |
|     
 | |
|     // Add 17 hidden axes (one per GPIO) to be able to shift (stack) graphs vertically
 | |
|     for (var gpio = 0; gpio <= 16; gpio++) { // 17 GPIOs
 | |
|       // Add a scale
 | |
|       digitalChart.options.scales.yAxes.push(
 | |
|         {
 | |
|           id: 'gpio' + gpio + '-axis',
 | |
|           type: 'linear',
 | |
|           display:false,
 | |
|           ticks: {
 | |
|             min:0,
 | |
|             max:2
 | |
|           }
 | |
|         }
 | |
|       );
 | |
|         
 | |
|       // Create a blank dataset for this gpio
 | |
|       allDigitalDatasets.push(createDigitalDataset(gpio));
 | |
|     }
 | |
| 
 | |
|     // Only show the first two Y axes (one for each side), with all tick labels showing only 0's and 1's (modulo)
 | |
|     for (var i = 0; i < 2; i++) {
 | |
|       digitalChart.options.scales.yAxes[i].display = true;
 | |
|       digitalChart.options.scales.yAxes[i].ticks.autoSkip = false;
 | |
|       digitalChart.options.scales.yAxes[i].ticks.stepSize = 1;
 | |
|       digitalChart.options.scales.yAxes[i].ticks.callback = function(value, index, values) {return Math.abs(value)%2;};
 | |
|     }
 | |
|     // Show gridLines of first axis
 | |
|     digitalChart.options.scales.yAxes[0].gridLines = {color: "#333333"};
 | |
| 
 | |
|     // Move second axis to the right
 | |
|     digitalChart.options.scales.yAxes[1].position = 'right';
 | |
|   }
 | |
| 
 | |
|   
 | |
|   // Compute a string with min/max/average on the given array of (x, y) points
 | |
|   function computeStats(sampleArray) {
 | |
|     var minY = Number.MAX_SAFE_INTEGER, maxY = 0, avgY = 0;       
 | |
|     var prevX = 0;
 | |
|     sampleArray.forEach(function (point, index) {
 | |
|       if (point.y < minY) minY = point.y;
 | |
|       if (point.y > maxY) maxY = point.y;
 | |
|       if (prevX > 0) avgY = avgY + point.y * (point.x - prevX); // avg is weighted: samples with a longer period weight more
 | |
|       prevX = point.x;
 | |
|     });
 | |
|     avgY = avgY / (prevX - sampleArray[0].x);
 | |
|     return "min: " + minY + ", max: " + maxY + ", avg: " + Math.floor(avgY);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /////////////////////////////////////
 | |
|   // PERIODIC FETCH OF NEW DATA
 | |
| 
 | |
|   var running = true;
 | |
|   var configDiv = document.getElementById("config");
 | |
|   var periodField = document.getElementById("periodField");
 | |
|   
 | |
|   var currentGpioMask = 0;
 | |
|   
 | |
|   function fetchNewData() {
 | |
|     if (!running) return;
 | |
|     var fetchStartMs = Date.now();
 | |
|     var xmlHttp = new XMLHttpRequest();
 | |
|     xmlHttp.onload = function() {
 | |
|       var processingDurationMs;
 | |
|       if(xmlHttp.status != 200) {
 | |
|         // Server error
 | |
|         configDiv.style.backgroundColor = "red";
 | |
|         console.log("GET ERROR: [" + xmlHttp.status+"] " + xmlHttp.responseText);
 | |
|         processingDurationMs = new Date() - fetchStartMs;
 | |
|       }
 | |
|       else {
 | |
|         configDiv.style.backgroundColor = "white";
 | |
| 
 | |
|         // Response is e.g.: {"time":963074, "heap":47160, "analog":60, "gpioMask":16447, "gpioData":16405}
 | |
|         var espData = JSON.parse(xmlHttp.responseText);
 | |
| 
 | |
|         var x = espData.time;
 | |
| 
 | |
|         var maxSamples = parseInt(document.getElementById("maxSamplesField").value);
 | |
|         
 | |
|         // Add point to heap chart
 | |
|         var data = heapChart.data.datasets[0].data;
 | |
|         data.push({x: espData.time, y: espData.heap});
 | |
|         // Limit its length to maxSamples
 | |
|         data.splice(0, data.length - maxSamples);
 | |
|         // Put stats in chart title
 | |
|         heapChart.options.title.text = "Free heap (" + computeStats(data) + ") - use mouse wheel or pinch to zoom";
 | |
|         // Remember smallest X
 | |
|         var minX = data[0].x;
 | |
|         
 | |
|         // Add point to analog chart
 | |
|         data = analogChart.data.datasets[0].data;
 | |
|         data.push({x: espData.time, y: espData.analog});
 | |
|         // Limit its length to maxSamples
 | |
|         data.splice(0, data.length - maxSamples);
 | |
|         // Put stats in chart title
 | |
|         analogChart.options.title.text = "Analog input (" + computeStats(data) + ") - use mouse wheel or pinch to zoom";
 | |
|         // Remember smallest X
 | |
|         if (data[0].x < minX) minX = data[0].x;
 | |
|         
 | |
| 
 | |
|         // If GPIO mask has changed, reconfigure chart
 | |
|         if (espData.gpioMask != currentGpioMask) {        
 | |
|           // Remove all datasets from chart
 | |
|           digitalChart.data.datasets.length = 0;
 | |
| 
 | |
|           // And re-add the required datasets                    
 | |
|           var position = 0;
 | |
|           for (var gpio = 0; gpio <= 16; gpio++) { // 17 GPIOs
 | |
|             var gpioBitMask = 1 << gpio;          
 | |
|                         
 | |
|             if (espData.gpioMask & gpioBitMask) {
 | |
|               // This GPIO must be part of the chart
 | |
|               dataset = allDigitalDatasets[gpio];
 | |
|               // give it the next available color
 | |
|               dataset.borderColor = gpioColors[position % gpioColors.length];
 | |
|               // add it to the chart
 | |
|               digitalChart.data.datasets.push(dataset);
 | |
|               
 | |
|               // offset graph
 | |
|               digitalChart.options.scales.yAxes[gpio].ticks.min = position * -2;
 | |
|               position++;
 | |
|             }
 | |
|             else {
 | |
|               // Clear data
 | |
|               allDigitalDatasets[gpio].data = [];
 | |
|             }
 | |
|           }
 | |
|               
 | |
|           // make sure all graphs have the same scale (max-min)
 | |
|           for (var gpio = 0; gpio <= 16; gpio++) { // 17 GPIOs
 | |
|             var ticks = digitalChart.options.scales.yAxes[gpio].ticks;
 | |
|             ticks.max = ticks.min + position * 2 - 1;
 | |
|           }
 | |
|           
 | |
|           // chart must be updated, otherwise adding to previously hidden dataset fail
 | |
|           digitalChart.update(); 
 | |
|           
 | |
|           currentGpioMask = espData.gpioMask
 | |
|         }
 | |
| 
 | |
| 
 | |
|         // And process each dataset, adding it back to the chart if requested, and adding the received value to it.
 | |
|         var position = 0;
 | |
|         for (var gpio = 0; gpio <= 16; gpio++) { // 17 GPIOs
 | |
|           var gpioBitMask = 1 << gpio;
 | |
|           
 | |
|           if (espData.gpioMask & gpioBitMask) {
 | |
|             // This GPIO must be part of the chart
 | |
|             var dataset = digitalChart.data.datasets[position];
 | |
|                            
 | |
|             // Add received value to dataset
 | |
| //            console.log(espData.gpioData + " & " + gpioBitMask + " => " + (espData.gpioData & gpioBitMask));
 | |
|             data = dataset.data;
 | |
|             var point = {x: espData.time, y: (espData.gpioData & gpioBitMask)?1:0}
 | |
|             data.push(point);
 | |
|             
 | |
|             // limit number of samples to maxSamples
 | |
|             data.splice(0, data.length - maxSamples);
 | |
|             // Remember smallest X
 | |
|             if (data[0].x < minX) minX = data[0].x;
 | |
|             
 | |
|             position++;
 | |
|           }         
 | |
|         }
 | |
| 
 | |
|         // Set origin of X axis to minimum X value
 | |
|         if (heapChart.options.scales.xAxes[0].ticks.min === undefined || heapChart.options.scales.xAxes[0].ticks.min < minX) {
 | |
|           heapChart.options.scales.xAxes[0].ticks.min = minX;
 | |
|           analogChart.options.scales.xAxes[0].ticks.min = minX;
 | |
|           digitalChart.options.scales.xAxes[0].ticks.min = minX;
 | |
|         }
 | |
| 
 | |
|         heapChart.update();
 | |
|         analogChart.update();
 | |
|         digitalChart.update(); 
 | |
| 
 | |
|         // Check if we can keep up with this fetch rate, or increase interval by 50ms if not.        
 | |
|         processingDurationMs = new Date() - fetchStartMs;
 | |
|         periodField.style.backgroundColor = (processingDurationMs > parseInt(periodField.value))?"#FF7777":"#FFFFFF";
 | |
|       }
 | |
|       
 | |
|       // Schedule next call with the requested interval minus the time needed to process the previous call
 | |
|       // Note that contrary to setInterval, this technique guarantees that we will never have 
 | |
|       // several fetch in parallel in case we can't keep up with the requested interval
 | |
|       var waitingDelayMs = parseInt(periodField.value) - processingDurationMs;
 | |
|       if (running) {
 | |
|         window.setTimeout(fetchNewData, (waitingDelayMs > 0)?waitingDelayMs:0);
 | |
|       }
 | |
|     };    
 | |
|     xmlHttp.onerror = function(e) {
 | |
|       // Ajax call error
 | |
|       configDiv.style.backgroundColor = "red";
 | |
|       console.log("ERROR: [" + xmlHttp.status+"] " + xmlHttp.responseText);
 | |
|       // Also schedule next call in case of error
 | |
|       var waitingDelayMs = parseInt(periodField.value) - (new Date() - fetchStartMs);
 | |
|       window.setTimeout(fetchNewData, (waitingDelayMs > 0)?waitingDelayMs:0);      
 | |
|     };
 | |
|     xmlHttp.open("GET", "/espData", true);
 | |
|     xmlHttp.send();
 | |
|   }
 | |
|   
 | |
|   /////////////////////////////////////
 | |
|   // EVENT HANDLERS
 | |
| 
 | |
|   // Keep range slider and text input in sync
 | |
|   document.getElementById('periodRange').addEventListener('input', function (e) {
 | |
|     document.getElementById('periodField').value = e.target.value;
 | |
|   });
 | |
|   document.getElementById('periodField').addEventListener('input', function (e) {
 | |
|     document.getElementById('periodRange').value = e.target.value;
 | |
|   });      
 | |
| 
 | |
|   // Start/pause button
 | |
|   document.getElementById('startPauseButton').addEventListener('click', function (e) {    
 | |
|     running = !running;
 | |
|     if (running) {
 | |
|       document.getElementById('startPauseButton').textContent = "Pause";
 | |
|       document.getElementById('clearButton').disabled = true;
 | |
|       fetchNewData();
 | |
|     }
 | |
|     else {
 | |
|       document.getElementById('startPauseButton').textContent = "Start";
 | |
|       document.getElementById('clearButton').disabled = false;
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   // Clear button
 | |
|   document.getElementById('clearButton').addEventListener('click', function (e) {
 | |
|     heapChart.data.datasets[0].data.length = 0;
 | |
|     analogChart.data.datasets[0].data.length = 0;
 | |
|     for (var gpio = 0; gpio <= 16; gpio++) { // 17 GPIOs
 | |
|       allDigitalDatasets[gpio].data = [];
 | |
|     }
 | |
|     heapChart.update();
 | |
|     analogChart.update();
 | |
|     digitalChart.update(); 
 | |
|   });
 | |
| 
 | |
| 
 | |
|   /////////////////////////////////////
 | |
|   // GET THE BALL ROLLING
 | |
|   
 | |
|   // Configure the digital chart
 | |
|   initDigitalChart();
 | |
| 
 | |
|   // Get first sample
 | |
|   fetchNewData();
 | |
|    
 | |
|   </script>
 | |
| </html>
 |