Try   HackMD

Final Project-第六週

tags: NKFW第二届

Final Project 是什麽?

今天最後要教給你們的就是六週上課的集大成!那我們要教什麽呢?
最後就是請你們整合做出一個動態的溫濕度感測器,然後在網頁上呈現
但不用擔心,我們會一步一步的讓你們回憶起這六週教的内容,慢慢引導你們怎麽實作!

如何將 ESP32 的資料傳送到網頁上顯示

我們這裡會使用的工具是以下這兩個協定:
1. EVENTSOURCE
2. EVENTSOURCELISTENER
這兩個是做什麽?簡單來說,就是透過網頁來觸發資料進行傳輸

那要怎麽使用呢?首先我們來看一下 Arduino 基本架構

#include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; }

首先要先加入函式庫:

#include <AsyncTCP.h>

如何設定觸發傳送的方法呢?就是使用早上講過的server.on(),透過路徑觸發傳送的方法,就可以以下的設定觸發所使用的路徑:

AsyncEventSource events("/events");

我們建立了這個 /event 的路徑作為觸發傳送資料的機制,寫法也跟我們在傳 HTML 幾乎一模一樣,
想想看,我們今天原本是傳送string html,那我們要怎麽包裝傳送資料?

JSONVar

這個時候我們就會用到了 json 這個檔案了!
json 基本上就是一個 dictionary 的概念:
就是我可以設定說濕度:XX,溫度:YY,然後丟不同的數字進去,在解析 JSON 時,就可以查到說這個變數對應的數值是多少了!

那一樣,讓我們看一下基本架構:

#include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; }

我們首先加入這個 library:

#include <Arduino_JSON.h>

然後宣告我們的物件為 readings:

JSONVar readings;

接著先撰寫好我們的 json 函式:

String getSensorReadings(){ //宣告這個函式回傳的種類是什麽 readings["temperature"] = String(dht.readTemperature(false)); // 設定變數和數值 readings["humidity"] = String(dht.readHumidity()); //設定變數和數值 String jsonString = JSON.stringify(readings); //將這個dictionary轉換成一個字串,這樣才能把數值正確回傳給函式 return jsonString; //回傳 string }

這個就是取代我們之前string html成為string json

最後就是如何request->send

server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); // "application" request->send(200, "application/json", json); json = String(); });

這邊要注意的是 /readings 是客戶端觸發的,而後續伺服器才是透過 /events 觸發連續的回傳資料

這裡我們可以看到
路徑改成透過/readings去進行觸發,那我們在[](AsyncWebServerRequest *request)就可以觸發我們剛剛寫的函式執行,得出json這個字串。
隨後的 request->send() 這邊的三個參數也就不適用 SPIFFS 的規格去撰寫,而是要用一般格式撰寫。
也就是第一個變數填 status code,第二個填你的 content type,也就是我們在早上有講過的,然後最後就是我們的資料 json

而最後把全部包裝起來的就是:

server.addHandler(&events);

加入這一行在 setup 裡面,代表現在伺服器有辦法進行 Event 的控制了!

現在看起來這個觸發 /readings 跟 /events 好像沒什麽關係,所以我們來解釋一下 EventSource 的運作原理:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

EventSource 會先透過客戶端開啓,伺服器就會開始不斷的傳送訊息給客戶端,所以這裡也代表了我們也需要在 js 做一些處理,
這一行就是在講上面的 new eventsource('some url'),也就是創建一個新的 EventSource

window.addEventListener('load', getReadings);

那這裡的 getReadings 又是什麽呢?getReadings就是啓動第一次的請求,如下所述:
簡單來説就是將收到的資料要如何進行處理,透過json進行傳輸之後透過parse將資料分割出來

function getReadings(){//COPY THIS FUNCTION var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myObj = JSON.parse(this.responseText); console.log(myObj); temperature = parseFloat(myObj.temperature); humidity = parseFloat(myObj.humidity); } }; xhr.open("GET", "/readings", true); xhr.send(); }

而這裡就是確實去聽 EventSource 的地方,我們將新的資料稱為 new_readings,後面則是定義 e 為 json 字串,然後透過 Parse 去把資料進行分割

source.addEventListener("new_readings", function(e) { console.log("new_readings", e.data); var myObj = JSON.parse(e.data); console.log(myObj); temperature =parseFloat(myObj.temperature); humidity = parseFloat(myObj.humidity); }, false);

最後在持續推送資料到客戶端又是怎麽實作的呢?
我們可以用 loop 來實作,並加上新的 events 函式,這樣就能達到持續傳送資料的機制了

events.send(getSensorReadings().c_str(),"new_readings" ,millis());

解釋一下 events.send() 兩個函式:
前面就是先把 getSensorReadings 這個函式轉換成event.send 所能使用的陣列(Array),接著定義名稱,最後就是 ID ,這裡是使用本身的 Arduino 的執行時間來做 ID

Final Project實作

在講完如何將資料從伺服器傳到客戶端後,我們可以開始規劃如何設計我們最後的計劃
一樣先來說明要做的步驟:

  1. 建立 WebServer 物件
  2. 建立 EventSource 物件
  3. 建立 JSONVar 物件
  4. 建立 DHT 物件
  5. 啓動 WIFI
  6. 開啓 SPIFFS
  7. 啓動 DHT22
  8. 建立 JSON 化資料的函式
  9. 在 PlatformIO 建立一個 spiffs 專用資料夾
  10. 新增 HTML/CSS/JAVASCRIPT 檔案
  11. 撰寫 HTML/CSS/JAVASCRIPT
  12. 燒錄 SPIFFS
  13. 設定預設 SPIFFS 路徑
  14. 設定網頁路徑
  15. 設定索取資料路徑
  16. 加入 Event Handler
  17. 開啓伺服器
  18. 處理透過 events 傳送資料
  19. 持續監控客戶的連線狀況

這裡一步一步來,先來回顧一下 Arduino 本身的開發架構:

#include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; }

導入函式庫

我們來想想,如果我們想要把之前的功能全部整合進去的話?
我們需要用的函式庫有哪些呢?大家可以嘗試看看填寫以下的填空哦!

#include <Arduino.h> #include <????.h>//跟無線網路有關的 #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "??????.h"//跟資料管理系統有關的 #include <Arduino_JSON.h> #include <DHT.h>

建立四大物件

我們要先定義物件,我想大家都很熟悉了

DHT dht(DHT針腳, DHT22); AsyncWebServer server(80); AsyncEventSource events("/events"); JSONVar readings;

啓動 WIFI

啓動 WIFI 我們必須先做三個步驟
我們可以多多想一下
以下是提示!

  1. 設定 WIFI 模式
  2. 開啓 WIFI
  3. 等待連接 WIFI
  4. 列印現在 IP 到序列埠
WiFi.mode(WiFi_???); WiFi.begin(????, ????); while (WiFi.????() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP());

開啓 SPIFFS

如果我們想要判別 SPIFFS 有沒有成功裝上去,判別式要如何寫呢?

void initSPIFFS() { if (SPIFFS.begin(true) != ???) { Serial.println("An error has occurred while mounting SPIFFS"); } Serial.println("SPIFFS mounted successfully"); }

啓動DHT22

dht.begin();

建立JSON化資料的函式

我們剛剛有提過,怎麽把我們想要的資料包裝成 json,以利我們之後進行資料傳輸,所以我們應該在中括號裡寫什麽?最後需要字串化是什麽呢?

String getSensorReadings(){ readings["????"] = String(dht.readTemperature(false)); readings["????"] = String(dht.readHumidity()); String jsonString = JSON.stringify(?????); return jsonString; }

在 PlatformIO 建立一個 spiffs 專用資料夾

我想大家都很聰明,都知道怎麽在旁邊加裝資料夾

新增 HTML/CSS/JAVASCRIPT 檔案

這個也很簡單,就是在 data 資料夾底下把三個檔案創建好!

撰寫 HTML/CSS/JAVASCRIPT

HTML

這裡我們會簡單的給大家一個模板可以直接使用

<!DOCTYPE HTML> <html> <head> <title>ESP IOT DASHBOARD</title> <script type="text/javascript" src="https://cdn.canvasjs.com/canvasjs.min.js"></script> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <div class="topnav"> <h1>ESP WEB SERVER GAUGES</h1> </div> <div class="content"> <div class="card-grid"> <div class="card"> <div id="TemperaturechartContainer"></div> </div> <div class="card"> <div id="HumiditychartContainer"></div> </div> </div> </div> <div class="footer"> <p>ESP IOT DASHBOARD</p> <script src="scripts.js"></script> </body> </html>

CSS

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 { /*background-image : url("https://static.gltjp.com/glt/prd/data/article/21000/20453/20231015_020307_a16455a9_w1920.jpg"); background-size : cover;*/ margin: 0; } .content { padding: 5%; } .card-grid { max-width: 80%; min-width: 400px; min-height: 300px; max-height: 500px; height: auto; margin: auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); } .card { position : relative; background-color: rgb(255, 255, 255); box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); border-radius: 7.5px; } .card-title { font-size: 1.2rem; font-weight: bold; color: #034078 }

Javascript

這邊需要你們使用今天教你們的東西了哦!這邊也會給大家提示!把問號的地方填上!

window.addEventListener('load', getReadings); var dataPoints = Array.from({length: 60}, (_, i) => { var date = new Date(); date.setSeconds(date.getSeconds() - (60 - i)); return { x: date, y: 0 }; }); var dataPoints2 = Array.from({length: 60}, (_, i) => { var date = new Date(); date.setSeconds(date.getSeconds() - (60 - i)); return { x: date, y: 0 }; }); var humidity = 10; var temperature = 10; window.onload = function () { var chart = new CanvasJS.Chart("HumiditychartContainer", { title:{ text: "Humidity" }, axisX: { interval: 1, intervalType: "minute", valueFormatString: "hh:mm" }, data: [ { type: "line", xValueType: "dateTime", dataPoints: dataPoints } ] }); chart.render(); setInterval(function () { // Get the current date and time var currentDate = new Date(); // Add a new data point dataPoints.push({ x: currentDate, y: humidity/*put the humidity here*/}); // Remove the oldest data point if there are more than 12 data points if (dataPoints.length > 62) { dataPoints.shift(); } // Update the chart chart.render(); }, 1000); // Update every minute var chart2 = new CanvasJS.Chart("TemperaturechartContainer", { title:{ text: "Temperature" }, axisX: { interval: 1, intervalType: "minute", valueFormatString: "hh:mm" }, data: [ { type: "line", xValueType: "dateTime", dataPoints: dataPoints2 } ] }); chart2.render(); setInterval(function () { // Get the current date and time var currentDate = new Date(); // Add a new data point dataPoints2.push({ x: currentDate, y: temperature/*put the temperature here*/ }); // Remove the oldest data point if there are more than 12 data points if (dataPoints2.length > 62) { dataPoints2.shift(); } // Update the chart chart2.render(); }, 1000); // Update every minute } // Function to get current readings on the webpage when it loads for the first time function getReadings(){//COPY THIS FUNCTION var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var myObj = JSON.parse(this.responseText); console.log(myObj); temperature = parseFloat(myObj.temperature); humidity = parseFloat(myObj.humidity); } }; xhr.open("GET", "????", true);//提示:這邊是一個我們在主程式裏面調取資料的路徑 xhr.send(); } if (!!window.EventSource) { var source = new ?????('/events');//提示這裏我們需要創建新的eventsource,那要如何處理呢? 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); temperature =parseFloat(myObj.temperature); humidity = parseFloat(myObj.humidity); }, false); }

設定預設SPIFFS路徑

server.serveStatic("/", SPIFFS, "/");

設定網頁路徑

這邊給一些提示
server.on() 的三個變數代表的意思可以回去看一下

// Web Server Root URL server.on("/", ?????, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); });

設定索取資料路徑

server.on("/readings", ?????, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); // "application" request->send(???, "????", ????); json = String(); });

加入event handler

server.addHandler(&events);

開啓伺服器

server.begin();

處理透過events傳送資料

void loop() { events.send(getSensorReadings().c_str(),"new_readings" ,millis()); }