1
0
mirror of https://github.com/esp8266/Arduino.git synced 2025-04-21 10:26:06 +03:00
vdeconinck c3796a4de5
Graph example (#7299)
* 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)
2020-06-02 20:59:16 -04:00

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&nbsp;number&nbsp;of&nbsp;samples:&nbsp;</span>
<input id="maxSamplesField" type="number" min="100" max="10000" value="1000" step="100" size="4" class="left" />
<span class="left">.&nbsp;Sampling&nbsp;period:&nbsp;</span>
<input id="periodField" type="number" min="0" max="10000" value="1000" step="100" size="4" class="left" />
<span class="left">&nbsp;ms&nbsp;</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>