mirror of
https://github.com/esp8266/Arduino.git
synced 2025-04-21 10:26:06 +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>
|