Last active
May 28, 2024 19:15
-
-
Save fadhilaf/0169875e0e93944f1b41803631b44b0d to your computer and use it in GitHub Desktop.
Simple soil moisture sensor HTML interface + esp32 server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <WiFi.h> | |
#include <AsyncTCP.h> | |
#include <ESPAsyncWebServer.h> | |
#define SSID "ssid" | |
#define PASSWORD "password" | |
#define moisture_pin 35 | |
AsyncWebServer server(80); | |
void setup() { | |
// Baud rate | |
Serial.begin(9600); | |
// Set up koneksi Wi-Fi | |
WiFi.begin(SSID, PASSWORD); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(1000); | |
Serial.println("Connecting to WiFi..."); | |
} | |
Serial.println("Connected to WiFi"); | |
// Set up routes | |
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { | |
request->send(200, "text/html", generateHTML()); | |
}); | |
// Define endpoint AJAX requests untuk tingkat kelembapan | |
server.on("/sensor", HTTP_GET, [](AsyncWebServerRequest *request) { | |
//baca sensor | |
int moistureLevel = analogRead(moisture_pin); | |
// Data response | |
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", String(moistureLevel)); | |
// Add CORS header | |
response->addHeader("Access-Control-Allow-Origin", "*"); | |
// Kirim | |
request->send(response); | |
}); | |
server.begin(); | |
} | |
void loop() { | |
Serial.println(analogRead(moisture_pin)); | |
Serial.println(WiFi.localIP()); | |
delay(10000); | |
} | |
String generateHTML() { | |
// Generate HTML content with variable values | |
String htmlContent = "<!doctypehtml><html lang=en><meta charset=UTF-8><meta content=\"width=device-width,initial-scale=1\"name=viewport><title>Sensor Kelembaban Tanah</title><script src=https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js></script><link href=https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css rel=stylesheet><style>#table-container{max-height:300px;overflow-y:auto}</style><h1>Sensor Kelembaban Tanah</h1><form id=dataForm><label for=interval>Update sensor per (ms 1000 untuk 1 detik):</label> <input id=interval><br><label for=queue>Jumlah data maksimal (60 untuk menyimpan satu menit jika update sensor per 1 detik):</label> <input id=queue><br><button id=submitButton type=button>Tampilkan sensor</button></form><canvas height=100 id=myChart width=400></canvas><div id=table-container><table class=\"table table-striped\"><thead><tr><th scope=col style=width:80%>Waktu<th scope=col style=width:10%><th scope=col style=width:10%><tbody id=table-body></table></div><div id=history></div><script>async function fetchData(){const t=Date.now();try{const e=await fetch("; | |
htmlContent += "\"http://" + WiFi.localIP().toString() + "/sensor\""; | |
htmlContent += ");if(e.ok){const a=await e.json();return[t,4095-parseInt(a)]}throw new Error(\"Network response was not ok.\")}catch(t){return console.error(\"Error fetching data:\",t),[]}}document.addEventListener(\"DOMContentLoaded\",function(){const t=document.getElementById(\"interval\"),e=document.getElementById(\"queue\"),a=document.getElementById(\"dataForm\"),n=document.getElementById(\"submitButton\"),r=document.getElementById(\"myChart\");n.addEventListener(\"click\",function(){const n=t.value.trim(),o=e.value.trim();\"\"!==n&&\"\"!==o?function(t,e){let n=new Chart(r,{type:\"line\",data:createChartData([],[],\"red\")});r.style.display=\"block\",a.style.display=\"none\",setInterval(()=>run(n,e),t)}(n,o):alert(\"Mohon lengkapi data.\")}),renderTable()});let intervalState=0;function updateChartData(t,e,a){addToQueue(t.data.labels,currentTime(e[0]),a),addToQueue(t.data.datasets[0].data,e[1],a),e[1]<2048?t.data.datasets[0].borderColor=\"red\":t.data.datasets[0].borderColor=\"green\",++intervalState==a&&(saveChartState(t,e[0],a),intervalState=0),t.update()}function saveChartState(t,e){let a,n=localStorage.getItem(\"sensorData\");(a=null==n?{}:JSON.parse(n))[e]={labels:t.data.labels,data:t.data.datasets[0].data},localStorage.setItem(\"sensorData\",JSON.stringify(a)),renderTable()}function deleteChartState(t){let e=localStorage.getItem(\"sensorData\");if(null!=e){const a=JSON.parse(e);delete a[t],localStorage.setItem(\"sensorData\",JSON.stringify(a)),renderTable()}}function renderTable(){document.getElementById(\"table-body\").innerHTML=\"\";let t=localStorage.getItem(\"sensorData\");if(null!=t){const e=JSON.parse(t);Object.keys(e).forEach(t=>{createRow(parseInt(t,10))})}}function createRow(t){const e=document.createElement(\"tr\");e.innerHTML=`\\n <td>${toFullStringDate(t)}</td>\\n <td><button class=\"btn btn-primary btn-sm\">Lihat</button></td>\\n <td><button class=\"btn btn-danger btn-sm\">Hapus</button></td>\\n `,e.querySelector(\".btn-danger\").addEventListener(\"click\",function(){deleteChartState(t)}),e.querySelector(\".btn-primary\").addEventListener(\"click\",function(){renderSavedChart(t)}),document.getElementById(\"table-body\").appendChild(e)}function renderSavedChart(t){let e,a=localStorage.getItem(\"sensorData\");if(t in(e=null==a?{}:JSON.parse(a))){const a=e[t],n=document.createElement(\"canvas\");new Chart(n,{type:\"line\",data:createChartData(a.labels,a.data,checkArray(a.data,2048)?\"green\":\"red\")});const r=document.createElement(\"div\");r.className=\"border m-3 p-3\",r.innerHTML=`\\n <h3>${toFullStringDate(t)}</h3>\\n `;const o=document.createElement(\"button\");o.addEventListener(\"click\",function(){r.remove()}),o.className=\"btn btn-secondary\",o.innerText=\"Tutup\",r.appendChild(n),r.appendChild(o),document.getElementById(\"history\").appendChild(r)}}async function run(t,e){updateChartData(t,await fetchData(),e)}function addToQueue(t,e,a){t.push(e);const n=a;t.length>n&&t.shift()}function currentTime(t){const e=new Date(t);return`${e.getHours().toString().padStart(2,\"0\")}:${e.getMinutes().toString().padStart(2,\"0\")}:${e.getSeconds().toString().padStart(2,\"0\")}`}function toFullStringDate(t){const e=new Date(t),a=e.getFullYear(),n=(\"0\"+(e.getMonth()+1)).slice(-2);return`${(\"0\"+e.getDate()).slice(-2)}/${n}/${a} ${(\"0\"+e.getHours()).slice(-2)}:${(\"0\"+e.getMinutes()).slice(-2)}:${(\"0\"+e.getSeconds()).slice(-2)}`}function createChartData(t,e,a){return{type:\"line\",labels:t,datasets:[{label:\"Tingkat kelembaban tanah\",data:e,borderColor:a,backgroundColor:\"rgba(0, 0, 255, 0.1)\",tension:0}]}}function checkArray(t,e){if(!Array.isArray(t)||0===t.length)throw new Error(\"Input must be a non-empty array of integers.\");return t.filter(t=>t>e).length/t.length*100>=80}</script><script src=https://code.jquery.com/jquery-3.5.1.slim.min.js></script><script src=https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js></script><script src=https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js></script>"; | |
return htmlContent; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Sensor Kelembaban Tanah</title> | |
<!-- CDN Chart.js --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js"></script> | |
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> | |
<style> | |
/* Styling for the table and scrollable container */ | |
#table-container { | |
max-height: 300px; /* Set the maximum height of the container */ | |
overflow-y: auto; /* Enable vertical scrolling */ | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Sensor Kelembaban Tanah</h1> | |
<form id="dataForm"> | |
<label for="ip">IP ESP32:</label> | |
<input type="text" id="ip"> | |
<br> | |
<label for="interval">Update sensor per (ms 1000 untuk 1 detik):</label> | |
<input type="text" id="interval"> | |
<br> | |
<label for="queue">Jumlah data maksimal (60 untuk menyimpan satu menit jika update sensor per 1 detik):</label> | |
<input type="text" id="queue"> | |
<br> | |
<button type="button" id="submitButton">Tampilkan sensor</button> | |
</form> | |
<!-- Elemen grafik --> | |
<canvas id="myChart" width="400" height="100"></canvas> | |
<!-- Elemen tabel --> | |
<div id="table-container"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th scope="col" style="width: 80%;">Waktu</th> | |
<th scope="col" style="width: 10%;"></th> | |
<th scope="col" style="width: 10%;"></th> | |
</tr> | |
</thead> | |
<tbody id="table-body"> | |
<!-- Table rows will be dynamically added here --> | |
</tbody> | |
</table> | |
</div> | |
<!-- Elemen histori data --> | |
<div id="history"></div> | |
<script src="script.js"></script> | |
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script> | |
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
document.addEventListener("DOMContentLoaded", function() { | |
const ip = document.getElementById("ip"); | |
const interval = document.getElementById("interval"); | |
const queue = document.getElementById("queue"); | |
const dataForm = document.getElementById("dataForm"); | |
const submitButton = document.getElementById("submitButton"); | |
const ctx = document.getElementById("myChart"); | |
submitButton.addEventListener("click", function() { | |
const ipData = ip.value.trim(); | |
const intervalData = interval.value.trim(); | |
const queueData = queue.value.trim(); | |
if (ipData !== "" && intervalData !== "" && queueData !== "") { | |
renderChart(ipData, intervalData, queueData); | |
} else { | |
alert("Mohon lengkapi data."); | |
} | |
}); | |
function renderChart(ipData, intervalData, queueData) { | |
let queue = []; | |
// Bentuk data | |
let myChart = new Chart(ctx, { | |
type: 'line', | |
data: createChartData([], queue, 'red'), | |
}); | |
// Menampilkan grafik | |
ctx.style.display = "block"; | |
dataForm.style.display = "none"; | |
setInterval(() => run(myChart, ipData, queueData), intervalData); //set IP | |
} | |
renderTable(); | |
}); | |
// Memanggil endpoint ESP32 | |
async function fetchData(ip) { | |
// URL ESP32 | |
const url = "http://" + ip | |
const time = Date.now(); | |
try { | |
const response = await fetch(url + '/sensor'); | |
if (response.ok) { | |
const responseData = await response.json(); | |
return [time, 4095 - parseInt(responseData)]; | |
} else { | |
throw new Error('Network response was not ok.'); | |
} | |
} catch (error) { | |
console.error('Error fetching data:', error); | |
return []; | |
} | |
} | |
let intervalState = 0; | |
function updateChartData(myChart, newData, queueData) { | |
addToQueue(myChart.data.labels, currentTime(newData[0]), queueData); | |
addToQueue(myChart.data.datasets[0].data, newData[1], queueData); | |
if (newData[1] < 2048) { | |
myChart.data.datasets[0].borderColor = 'red'; | |
} else { | |
myChart.data.datasets[0].borderColor = 'green'; | |
} | |
intervalState++; | |
if (intervalState == queueData) { | |
saveChartState(myChart, newData[0], queueData); | |
intervalState = 0; | |
} | |
myChart.update(); | |
} | |
function saveChartState(chart, key) { | |
let currentSensorDataJson = localStorage.getItem('sensorData'); | |
let currentDataObject; | |
if (currentSensorDataJson == null) { | |
currentDataObject = {}; | |
} | |
else { | |
currentDataObject = JSON.parse(currentSensorDataJson); | |
} | |
currentDataObject[key] = { | |
labels: chart.data.labels, | |
data: chart.data.datasets[0].data, | |
} | |
localStorage.setItem('sensorData', JSON.stringify(currentDataObject)); | |
renderTable(); | |
} | |
function deleteChartState(key) { | |
let currentSensorDataJson = localStorage.getItem('sensorData'); | |
if (currentSensorDataJson != null) { | |
const currentDataObject = JSON.parse(currentSensorDataJson); | |
delete currentDataObject[key]; | |
localStorage.setItem('sensorData', JSON.stringify(currentDataObject)); | |
renderTable() | |
} | |
} | |
function renderTable() { | |
document.getElementById('table-body').innerHTML = ""; | |
let currentSensorDataJson = localStorage.getItem('sensorData'); | |
if (currentSensorDataJson != null) { | |
const currentDataObject = JSON.parse(currentSensorDataJson); | |
Object.keys(currentDataObject).forEach(key => { | |
createRow(parseInt(key, 10)); | |
}) | |
} | |
} | |
function createRow(key) { | |
const newRow = document.createElement('tr'); | |
newRow.innerHTML = ` | |
<td>${toFullStringDate(key)}</td> | |
<td><button class="btn btn-primary btn-sm">Lihat</button></td> | |
<td><button class="btn btn-danger btn-sm">Hapus</button></td> | |
`; | |
// Add delete functionality to the new row's delete button | |
newRow.querySelector('.btn-danger').addEventListener('click', function() { | |
deleteChartState(key); | |
}); | |
newRow.querySelector('.btn-primary').addEventListener('click', function() { | |
renderSavedChart(key); | |
}); | |
// Append the new row to the table body | |
document.getElementById('table-body').appendChild(newRow); | |
} | |
function renderSavedChart(key) { | |
let currentSensorDataJson = localStorage.getItem('sensorData'); | |
let currentDataObject; | |
if (currentSensorDataJson == null) { | |
currentDataObject = {}; | |
} | |
else { | |
currentDataObject = JSON.parse(currentSensorDataJson); | |
} | |
if (key in currentDataObject) { | |
const chartData = currentDataObject[key]; | |
const newCanvas = document.createElement('canvas'); | |
new Chart(newCanvas, { | |
type: 'line', | |
data: createChartData(chartData.labels, chartData.data, checkArray(chartData.data, 2048) ? "green" : "red"), | |
}); | |
const newDiv = document.createElement('div'); | |
newDiv.className = 'border m-3 p-3'; | |
newDiv.innerHTML = ` | |
<h3>${toFullStringDate(key)}</h3> | |
`; | |
const newButton = document.createElement('button'); | |
newButton.addEventListener('click', function() { | |
newDiv.remove(); | |
}); | |
newButton.className = "btn btn-secondary"; | |
newButton.innerText = "Tutup"; | |
newDiv.appendChild(newCanvas) | |
newDiv.appendChild(newButton) | |
document.getElementById('history').appendChild(newDiv) | |
} | |
} | |
async function run(myChart, ip, queueData) { | |
const newData = await fetchData(ip); | |
updateChartData(myChart, newData, queueData); | |
} | |
// Utility functions | |
// Memperbarui antrian data | |
function addToQueue(queue, newData, queueData) { | |
queue.push(newData); | |
const maxSize = queueData; // Muatan maksimum | |
if (queue.length > maxSize) { | |
queue.shift(); | |
} | |
} | |
// Menghasilkan string waktu sekarang | |
function currentTime(timestamp) { | |
const date = new Date(timestamp); | |
let hours = date.getHours().toString().padStart(2, '0'); | |
let minutes = date.getMinutes().toString().padStart(2, '0'); | |
let seconds = date.getSeconds().toString().padStart(2, '0'); | |
return `${hours}:${minutes}:${seconds}`; | |
} | |
// Menghasilkan string tanggal lengkap | |
function toFullStringDate(timestamp) { | |
const date = new Date(timestamp); | |
const year = date.getFullYear(); | |
const month = ('0' + (date.getMonth() + 1)).slice(-2); // Months are zero-indexed | |
const day = ('0' + date.getDate()).slice(-2); | |
const hours = ('0' + date.getHours()).slice(-2); | |
const minutes = ('0' + date.getMinutes()).slice(-2); | |
const seconds = ('0' + date.getSeconds()).slice(-2); | |
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`; | |
} | |
function createChartData(labels, data, color) { | |
return { | |
type: 'line', | |
labels: labels, | |
datasets: [{ | |
label: 'Tingkat kelembaban tanah', | |
data: data, | |
borderColor: color, | |
backgroundColor: 'rgba(0, 0, 255, 0.1)', | |
tension: 0, | |
}] | |
}; | |
} | |
function checkArray(arr, threshold) { | |
if (!Array.isArray(arr) || arr.length === 0) { | |
throw new Error("Input must be a non-empty array of integers."); | |
} | |
const countAboveThreshold = arr.filter(item => item > threshold).length; | |
const percentage = (countAboveThreshold / arr.length) * 100; | |
return percentage >= 80; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <WiFi.h> | |
#include <AsyncTCP.h> | |
#include <ESPAsyncWebServer.h> | |
#define SSID "ssid" | |
#define PASSWORD "pass" | |
#define moisture_pin 35 | |
AsyncWebServer server(80); | |
void setup() { | |
// Baud rate | |
Serial.begin(9600); | |
// Set up koneksi Wi-Fi | |
WiFi.begin(SSID, PASSWORD); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(1000); | |
Serial.println("Connecting to WiFi..."); | |
} | |
Serial.println("Connected to WiFi"); | |
// Define endpoint AJAX requests untuk tingkat kelembapan | |
server.on("/sensor", HTTP_GET, [](AsyncWebServerRequest *request) { | |
//baca sensor | |
int moistureLevel = analogRead(moisture_pin); | |
// Data response | |
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", String(moistureLevel)); | |
// Add CORS header | |
response->addHeader("Access-Control-Allow-Origin", "*"); | |
// Kirim | |
request->send(response); | |
}); | |
server.begin(); | |
} | |
void loop() { | |
Serial.println(analogRead(moisture_pin)); | |
Serial.println(WiFi.localIP()); | |
delay(10000); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment