ESP32 Web Server: Hiển thị thông tin cảm biến bằng biểu đồ Gauge
Bài viết này hướng dẫn bạn cách tạo một ESP32 web server, cho phép hiển thị thông tin đọc được từ cảm biến dưới dạng biểu đồ gauge. Ví dụ, chúng ta sẽ hiển thị nhiệt độ và độ ẩm từ cảm biến BME280 bằng 2 biểu đồ: kiểu linear và kiểu radial. Bạn có thể chỉnh sửa tùy biến chương trình để hiển thị các dữ liệu khác theo ý muốn.
Để xây dựng biểu đồ gauge, chúng ta cần sử dụng thư viện JavaScript canvas-gauges cho trang web.
Tổng quan cách làm
Bài viết này hướng dẫn bạn tạo web server cho ESP32 hiển thị nhiệt độ và độ ẩm đọc được từ cảm biến BME280. Chúng ta sẽ tạo ra một biểu đồ gauge kiểu linear trông giống một nhiệt kế để hiển thị nhiệt độ, và kiểu radial để hiển thị độ ẩm.
Server-Sent Events
Thông tin cập nhật mới nhất đọc được từ cảm biến sẽ được gửi tới trang web bằng Server-Sent Events (SSE).
Tổ chức file bằng Filesystem của ESP32
Vì dự án này gồm nhiều file khác nhau ngoài file code, chúng ta cần tổ chức và sắp xếp các file một cách dễ hiểu sử dụng filesystem SPIFFS của ESP32.
Yêu cầu
Để thực hiện được theo bài hướng dẫn này, bạn cần phải cài đặt Arduino IDE và addon cho ESP32/ Bạn có thể tham khảo các bài viết sau:
Cài đặt thư viện trong Arduino IDE
Bạn cần cài đặt thêm các thư viện sau trong Arduino IDE để setup web server
- ESPAsyncWebServer (.zip folder)
- AsyncTCP (.zip folder)
- Adafruit_BME280 (Arduino Library Manager)
- Adafruit_Sensor library (Arduino Library Manager)
- Arduino_JSON library by Arduino version 0.1.0 (Arduino Library Manager)
Thư viện ESPAsyncWebServer, AsynTCP, và ESPAsyncTCP không có sẵn trong Arduino Library Manager, bạn cần phải download về máy và cài đặt trong Arduino thông qua menu Sketch > Include Library > Add .zip Library.
Với các thư viện có sẵn trong Arduino Library Manager, bạn có thể cài đặt bằng cách vào menu Sketch > Include Library > Manage Libraries và tìm kiếm theo tên để cài đặt.
Filesystem Uploader
Ngoài ra, bạn cũng cần cài đặt thêm một plugin cho Arduino IDE có tên là ESP32 Uploader. Bạn có thể xem hướng dẫn qua bài viết dưới:
Linh kiện cần có
Bài viết sử dụng các linh kiện sau, bạn cần chuẩn bị sẵn để có thể làm theo được:
- Board lập trình ESP32
- Cảm biến BME280
- Các dây jumper và breadboard
Bạn có thể dùng bất kỳ cảm biến nào khác và hiển thị các thông tin theo yêu cầu của mình. Nếu không có sẵn cảm biến, bạn cũng có thể cho hiển ra các số random để thử nghiệm chương trình.
Sơ đồ kết nối
Tổ chức các file của dự án
Dự án này gồm các file sau để có thể tạo được một web server cho ESP32:
- Arduino sketch chứa mã nguồn của chương trình cho ESP32
- index.html: chứa mã nguồn HTML của trang web;
- sytle.css: chưa file stylesheet giúp trang web nhìn đẹp hơn;
- script.js: chứa mã nguồn Javascript của trang web, giúp xử lý các event giao tiếp với web server, hiển thị thông tin trên các biểu đồ…
Các file thuộc loại HTML, CSS, và JavaScript sẽ được chứa trong thư mục chung là data trong thư mục của dự án. Chúng ta sẽ upload các file này vào filesystem của ESP32 (SPIFFS).
File HTML
Bạn copy và dán nội dung sau vào file index.html.
<!DOCTYPE html> <html> <head> <title>ESP IOT DASHBOARD</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/png" href="favicon.png"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="style.css"> <script src="http://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.7/all/gauge.min.js"></script> </head> <body> <div class="topnav"> <h1>ESP WEB SERVER GAUGES</h1> </div> <div class="content"> <div class="card-grid"> <div class="card"> <p class="card-title">Temperature</p> <canvas id="gauge-temperature"></canvas> </div> <div class="card"> <p class="card-title">Humidity</p> <canvas id="gauge-humidity"></canvas> </div> </div> </div> <script src="script.js"></script> </body> </html>
File HTML cho dự án này khá đơn giản. Nó bao gồm thư viện Javascript canvas-gauges trong thẻ head ở đầu file:
<script src="https://cdn.rawgit.com/Mikhus/canvas-gauges/gh-pages/download/2.1.7/all/gauge.min.js"></script>
Tạo một thẻ <canvas> với id là gauge-temperature để vẽ biểu đồ cho nhiệt độ.
<canvas id="gauge-temperature"></canvas>
Đồng thời tạo thêm một thẻ <canvas> khác với id gauge-humidity cho độ ẩm.
<canvas id="gauge-humidity"></canvas>
File CSS
Copy nội dung sau vào file style.css file. File css giúp format và làm trang web hiển thị đẹp hơn theo ý muốn của mình.
html { font-family: Arial, Helvetica, sans-serif; display: inline-block; text-align: center; } h1 { font-size: 1.8rem; color: white; } p { font-size: 1.4rem; } .topnav { overflow: hidden; background-color: #0A1128; } body { margin: 0; } .content { padding: 5%; } .card-grid { max-width: 1200px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .card-title { font-size: 1.2rem; font-weight: bold; color: #034078 }
File JavaScript
Copy nội dung sau vào file script.js.
// Get current sensor readings when the page loads window.addEventListener('load', getReadings); // Create Temperature Gauge var gaugeTemp = new LinearGauge({ renderTo: 'gauge-temperature', width: 120, height: 400, units: "Temperature C", minValue: 0, startAngle: 90, ticksAngle: 180, maxValue: 40, colorValueBoxRect: "#049faa", colorValueBoxRectEnd: "#049faa", colorValueBoxBackground: "#f1fbfc", valueDec: 2, valueInt: 2, majorTicks: [ "0", "5", "10", "15", "20", "25", "30", "35", "40" ], minorTicks: 4, strokeTicks: true, highlights: [ { "from": 30, "to": 40, "color": "rgba(200, 50, 50, .75)" } ], colorPlate: "#fff", colorBarProgress: "#CC2936", colorBarProgressEnd: "#049faa", borderShadowWidth: 0, borders: false, needleType: "arrow", needleWidth: 2, needleCircleSize: 7, needleCircleOuter: true, needleCircleInner: false, animationDuration: 1500, animationRule: "linear", barWidth: 10, }).draw(); // Create Humidity Gauge var gaugeHum = new RadialGauge({ renderTo: 'gauge-humidity', width: 300, height: 300, units: "Humidity (%)", minValue: 0, maxValue: 100, colorValueBoxRect: "#049faa", colorValueBoxRectEnd: "#049faa", colorValueBoxBackground: "#f1fbfc", valueInt: 2, majorTicks: [ "0", "20", "40", "60", "80", "100" ], minorTicks: 4, strokeTicks: true, highlights: [ { "from": 80, "to": 100, "color": "#03C0C1" } ], colorPlate: "#fff", borderShadowWidth: 0, borders: false, needleType: "line", colorNeedle: "#007F80", colorNeedleEnd: "#007F80", needleWidth: 2, needleCircleSize: 3, colorNeedleCircleOuter: "#007F80", needleCircleOuter: true, needleCircleInner: false, animationDuration: 1500, animationRule: "linear" }).draw(); // Function to get current readings on the webpage when it loads for the first time function getReadings(){ var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myObj = JSON.parse(this.responseText); console.log(myObj); var temp = myObj.temperature; var hum = myObj.humidity; gaugeTemp.value = temp; gaugeHum.value = hum; } }; xhr.open("GET", "/readings", true); xhr.send(); } if (!!window.EventSource) { var source = new EventSource('/events'); source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false); source.addEventListener('new_readings', function(e) { console.log("new_readings", e.data); var myObj = JSON.parse(e.data); console.log(myObj); gaugeTemp.value = myObj.temperature; gaugeHum.value = myObj.humidity; }, false); }
Dưới đây là tóm tắt các hành động chính trong file javascript trên:
- Khởi tạo giao thức server source event
- Khai báo xử lý khi nhận được sự kiện new_readings
- Tạo các biểu đồ gauge để hiển thị dữ liệu
- Đọc thông tin mới nhất của cảm biến trong sự kiện new_readings và hiển thị lên các biểu đồ gauge
- Gọi HTTP GET request về web server để lấy thông tin cảm biến lần đầu tiên khi người dùng truy cập trang web.
Lấy thông tin cảm biến
Khi người dùng truy cập trang web lần đầu, chúng ta sẽ gửi yêu cầu lên web server để lấy thông tin mới nhất của cảm biến. Nếu không, người dùng sẽ phải đợi web server gửi thông tin ở lần cập nhật theo định kỳ (qua giao thức Server-Sent Events), và điều này sẽ gây bất tiện cho người dùng.
Trang web sẽ đăng ký gọi getReadings sau khi trang web load xong.
// Get current sensor readings when the page loads window.addEventListener('load', getReadings);
Đối tượng window chính là đại diện cho 1 cửa sổ tab của trình duyệt. Hàm addEventListener() giúp trang web đăng ký lắng nghe các sự kiện của trình duyệt, trong đó load là sự kiện khi trang web đã load lên thành công.
Chúng ta cùng tìm hiểu hàm getReadings. Hàm này tạo một đối tượng XMLHttpRequest và gửi GET request đến server với đường dẫn /readings.
function getReadings() { var xhr = new XMLHttpRequest(); xhr.open("GET", "/readings", true); xhr.send(); }
Sau khi gửi đi, nếu ESP xử lý yêu cầu thành công thì sẽ trả về kết quả là thông tin của cảm biến. Chúng ta cần xử lý kết quả trả về này trong sự kiện onreadystatechange.
- readyState = 4 nghĩa là request được xử lý thành côngvà đã có kết quả trả về
- status = 200 nghĩa là “OK” và không có lỗi xảy ra
function getStates(){ var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { … DO WHATEVER YOU WANT WITH THE RESPONSE … } }; xhr.open("GET", "/states", true); xhr.send(); }
Kết quả trả về bởi ESP32 dưới dạng JSON như sau:
{ "temperature" : "25.02", "humidity" : "64.01", }
Chúng ta cần đổi từ chuỗi JSON sang đối tượng kiểu JSON sử dụng hàm parse() của Javascript.
var myObj = JSON.parse(this.responseText);
Biến myObj là một đối tượng kiểu JSON chứa thông tin về nhiệt độ và độ ẩm đọc được từ cảm biến. Chúng ta sẽ update các thông tin mới này lên biểu đồ.
gaugeTemp.value = myObj.temperature;
Tương tự cho thông tin độ ẩm.
gaugeHum.value = myObj.humidity;
Đây là toàn bộ hàm getReadings().
function getReadings(){ var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myObj = JSON.parse(this.responseText); console.log(myObj); var temp = myObj.temperature; var hum = myObj.humidity; gaugeTemp.value = temp; gaugeHum.value = hum; } }; xhr.open("GET", "/readings", true); xhr.send(); }
Tạo biểu đồ Gauge
Thư viện canvas-charts cho phép chúng ta tạo ra các biểu đồ gauge dạng linear và radial để hiển thị dữ liệu. Ba5nc ó thể tham khảo các ví dụ của thư viện này ở đây:
Biểu đồ cho nhiệt độ
Đoạn code Javascript sau tạo va hiển thị dữ liệu nhiệt độ lên biểu đồ gauge kiểu linear.
// Create Temperature Gauge var gaugeTemp = new LinearGauge({ renderTo: 'gauge-temperature', width: 120, height: 400, units: "Temperature C", minValue: 0, startAngle: 90, ticksAngle: 180, maxValue: 40, colorValueBoxRect: "#049faa", colorValueBoxRectEnd: "#049faa", colorValueBoxBackground: "#f1fbfc", valueDec: 2, valueInt: 2, majorTicks: [ "0", "5", "10", "15", "20", "25", "30", "35", "40" ], minorTicks: 4, strokeTicks: true, highlights: [ { "from": 30, "to": 40, "color": "rgba(200, 50, 50, .75)" } ], colorPlate: "#fff", colorBarProgress: "#CC2936", colorBarProgressEnd: "#049faa", borderShadowWidth: 0, borders: false, needleType: "arrow", needleWidth: 2, needleCircleSize: 7, needleCircleOuter: true, needleCircleInner: false, animationDuration: 1500, animationRule: "linear", barWidth: 10, }).draw();
Các tham số cũng khá là dễ hiểu dựa vào tên từng tham số trong phần khai báo. Ví dụ 2 tham số sau là khai báo giá trị min và max của biểu đồ:
minValue: 0,
maxValue: 40,
Chúng ta khai báo thêm các mốc chính sẽ được đánh dấu trên biểu đồ.
majorTicks: [ "0", "5", "10", "15", "20", "25", "30", "35", "40" ],
Chúng ta cũng làm tương tự cho độ ẩm nhưng với loại khác là radial.
// Create Humidity Gauge var gaugeHum = new RadialGauge({ renderTo: 'gauge-humidity', width: 300, height: 300, units: "Humidity (%)", minValue: 0, maxValue: 100, colorValueBoxRect: "#049faa", colorValueBoxRectEnd: "#049faa", colorValueBoxBackground: "#f1fbfc", valueInt: 2, majorTicks: [ "0", "20", "40", "60", "80", "100" ], minorTicks: 4, strokeTicks: true, highlights: [ { "from": 80, "to": 100, "color": "#03C0C1" } ], colorPlate: "#fff", borderShadowWidth: 0, borders: false, needleType: "line", colorNeedle: "#007F80", colorNeedleEnd: "#007F80", needleWidth: 2, needleCircleSize: 3, colorNeedleCircleOuter: "#007F80", needleCircleOuter: true, needleCircleInner: false, animationDuration: 1500, animationRule: "linear" }).draw();
Xử lý sự kiện
Khai báo sự kiện và URL của trang web nhận sự kiện là /events.
if (!!window.EventSource) { var source = new EventSource('/events');
Sau khi khởi tạo sự kiện, khai báo lắng nghe cập nhật từ server cho trang web bằng hàm addEventListener() .
source.addEventListener('open', function(e) { console.log("Events Connected"); }, false); source.addEventListener('error', function(e) { if (e.target.readyState != EventSource.OPEN) { console.log("Events Disconnected"); } }, false); source.addEventListener('message', function(e) { console.log("message", e.data); }, false);
Khai báo hàm xử lý sự kiện.
source.addEventListener('new_readings', function(e) {
source.addEventListener('new_readings', function(e) { console.log("new_readings", e.data); var myObj = JSON.parse(e.data); console.log(myObj); gaugeTemp.value = myObj.temperature; gaugeHum.value = myObj.humidity; }, false);
Chúng ta cũng in ra cửa sổ console log của trình duyệt để dễ tìm lỗi nếu có và hiển thị lên biểu đồ.
Chương trình trong Arduino
Copy đoạn code sau vào trong chương trình Arduino IDE.
#include <Arduino.h> #include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "SPIFFS.h" #include <Arduino_JSON.h> #include <Adafruit_BME280.h> #include <Adafruit_Sensor.h> // Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Create an Event Source on /events AsyncEventSource events("/events"); // Json Variable to Hold Sensor Readings JSONVar readings; // Timer variables unsigned long lastTime = 0; unsigned long timerDelay = 10000; // Create a sensor object Adafruit_BME280 bme; // BME280 connect to ESP32 I2C (GPIO 21 = SDA, GPIO 22 = SCL) // Init BME280 void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } } // Get Sensor Readings and return JSON object String getSensorReadings(){ readings["temperature"] = String(bme.readTemperature()); readings["humidity"] = String(bme.readHumidity()); String jsonString = JSON.stringify(readings); return jsonString; } // Initialize SPIFFS void initSPIFFS() { if (!SPIFFS.begin()) { Serial.println("An error has occurred while mounting SPIFFS"); } Serial.println("SPIFFS mounted successfully"); } // Initialize WiFi void initWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); while (WiFi.status() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); } void setup() { // Serial port for debugging purposes Serial.begin(115200); initBME(); initWiFi(); initSPIFFS(); // Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); }); server.serveStatic("/", SPIFFS, "/"); // Request for the latest sensor readings server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); request->send(200, "application/json", json); json = String(); }); events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // send event with message "hello!", id current millis // and set reconnect delay to 1 second client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events); // Start server server.begin(); } void loop() { if ((millis() - lastTime) > timerDelay) { // Send Events to the client with the Sensor Readings Every 10 seconds events.send("ping",NULL,millis()); events.send(getSensorReadings().c_str(),"new_readings" ,millis()); lastTime = millis(); } }
Giải thích chương trìnhy
Khai báo các thư viện cần sử dụng để làm việc với cảm biến BME280.
#include <Adafruit_BME280.h> #include <Adafruit_Sensor.h>
Và các thư viện WiFi, ESPAsyncWebServer, và AsyncTCP dùng để tạo web server.
#include <WiFi.h> #include <AsyncTCP.h> #include <ESPAsyncWebServer.h>
Khai báo thư viện để làm việc với các file lưu trong SPIFFS và dữ liệu kiểu Json.
#include "SPIFFS.h"
#include <Arduino_JSON.h>
Bạn cần thay đổi thông tin về mạng WiFi của mình cho chính xác để ESP32 kết nối vào WiFi được.
const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD";
AsyncWebServer và AsyncEventSource
Tạo một AsyncWebServer và chạy trên port 80.
AsyncWebServer server(80);
Tạo một event source ở URL là /events.
AsyncEventSource events("/events");
Khai báo các biến
Biến readings là một đối tượng JSON chứa các thông tin từ cảm biến.
JSONVar readings;
Các biến lastTime và timerDelay dùng để cấu hình tần suất và lần cuối cập nhật thông tin cảm biến với đơn vị là milli giây. Ví dụ nếu chúng ta cho ESP32 cập nhật cảm biến mỗi 30s thì biến timerDelay là 30000.
unsigned long lastTime = 0; unsigned long timerDelay = 30000;
Khởi tạo cảm biến.
// Init BME280 void initBME(){ if (!bme.begin(0x76)) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } }
Hàm đọc giá trị cảm biến
// Get Sensor Readings and return JSON object String getSensorReadings(){ readings["temperature"] = String(bme.readTemperature()); readings["humidity"] = String(bme.readHumidity()); String jsonString = JSON.stringify(readings); return jsonString; }
setup()
Khởi tạo Serial Monitor, Wi-Fi, filesystem và cảm biến.
void setup() { // Serial port for debugging purposes Serial.begin(115200); initBME(); initWiFi(); initSPIFFS();
Xử lý yêu cầu từ web
Khi người dùng truy cập vào trang chủ của web bằng URL /, ESP32 gửi trang web lưu trong file index.html để hiển thị lên trình duyệt.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); });
Khai báo cho phép web truy cập các file tĩnh khác như CSS (style.css) và Javascript (script.js) lưu trong SPIFFS của ESP32.
server.serveStatic("/", SPIFFS, "/");
Gửi chuỗi JSON chứa thông tin cảm biến đọc được khi có yêu cầu gửi đến URL /readings.
// Request for the latest sensor readings server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); request->send(200, "application/json", json); json = String(); });
Server Event Source
Cài đặt event source cho web server.
events.onConnect([](AsyncEventSourceClient *client){ if(client->lastId()){ Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); } // send event with message "hello!", id current millis // and set reconnect delay to 1 second client->send("hello!", NULL, millis(), 10000); }); server.addHandler(&events);
Bắt đầu chạy web server.
server.begin();
loop()
Trong hàm loop(), gửi thông tin cảm biến đọc được tới trang web mỗi 30 giây với tên event là new_readings.
events.send("ping",NULL,millis()); events.send(getSensorReadings().c_str(),"new_readings" ,millis());
Gửi ping đến client mỗi 30 giây giúp giữ kết nối dù việc nảy là không hoàn toàn bắt buộc.
events.send("ping",NULL,millis());
Uploading code và các file
bạn vào menu Sketch > Show Sketch Folder, và tạo một thư mục tên là data.
Inside that folder, you should save the HTML, CSS, and JavaScript files.
Tiến hành upload code.
Sau khi hoàn thành, bạn vào Tools > ESP32 Data Sketch Upload và chờ file được upload vào ESP32.
Sau khi upload thành công, mở của sổ Serial Monitor với baud rate 115200 và đọc địa chỉ IP của ESP32 nếu kết nối wIfI thành công.
Chạy Demo
Mở trình duyệt và gõ vào IP của ESP32. Bạn sẽ thấy giao diện trang web như sau:
Bạn cũng có thể tiến hành xem thông tin thông qua điện thoại như sau:
Lời kết
Trong bài viết này, mình đã hướng dẫn các bạn cách xây dựng một web server cho ESP32 để hiển thị thông tin cảm biến trên giao diện web bằng các biểu đồ gauge khá đẹp và trực quan. Bạn hoàn toàn có thể tùy biến để dùng cho các dự án khác với các loại cảm biến và thông tin khác tùy ý.