# Final Project-第六週 ###### tags: `NKFW第二届` ## Final Project 是什麽? 今天最後要教給你們的就是六週上課的集大成!那我們要教什麽呢? 最後就是請你們整合做出一個動態的溫濕度感測器,然後在網頁上呈現 但不用擔心,我們會一步一步的讓你們回憶起這六週教的内容,慢慢引導你們怎麽實作! ## 如何將 ESP32 的資料傳送到網頁上顯示 我們這裡會使用的工具是以下這兩個協定: **1. EVENTSOURCE** **2. EVENTSOURCELISTENER** 這兩個是做什麽?簡單來說,就是透過網頁來觸發資料進行傳輸 那要怎麽使用呢?首先我們來看一下 Arduino 基本架構 ```cpp= #include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; } ``` 首先要先加入函式庫: ```cpp= #include <AsyncTCP.h> ``` 如何設定觸發傳送的方法呢?就是使用早上講過的`server.on()`,透過路徑觸發傳送的方法,就可以以下的設定觸發所使用的路徑: ```cpp= AsyncEventSource events("/events"); ``` 我們建立了這個 /event 的路徑作為觸發傳送資料的機制,寫法也跟我們在傳 HTML 幾乎一模一樣, 想想看,我們今天原本是傳送`string html`,那我們要怎麽包裝傳送資料? ## JSONVar 這個時候我們就會用到了 json 這個檔案了! json 基本上就是一個 dictionary 的概念: 就是我可以設定說`濕度:XX,溫度:YY`,然後丟不同的數字進去,在解析 JSON 時,就可以查到說這個變數對應的數值是多少了! 那一樣,讓我們看一下基本架構: ```cpp= #include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; } ``` 我們首先加入這個 library: ```cpp= #include <Arduino_JSON.h> ``` 然後宣告我們的物件為 readings: ```cpp= JSONVar readings; ``` 接著先撰寫好我們的 json 函式: ```cpp= 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` ```cpp= server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); // "application" request->send(200, "application/json", json); json = String(); }); ``` :::warning 這邊要注意的是 `/readings` 是客戶端觸發的,而後續伺服器才是透過 `/events` 觸發連續的回傳資料 ::: 這裡我們可以看到 路徑改成透過`/readings`去進行觸發,那我們在`[](AsyncWebServerRequest *request)`就可以觸發我們剛剛寫的函式執行,得出json這個字串。 隨後的 request->send() 這邊的三個參數也就不適用 SPIFFS 的規格去撰寫,而是要用一般格式撰寫。 也就是第一個變數填 status code,第二個填你的 content type,也就是我們在早上有講過的,然後最後就是我們的資料 json 而最後把全部包裝起來的就是: ```cpp= server.addHandler(&events); ``` 加入這一行在 setup 裡面,代表現在伺服器有辦法進行 Event 的控制了! 現在看起來這個觸發 /readings 跟 /events 好像沒什麽關係,所以我們來解釋一下 EventSource 的運作原理: ![image](https://hackmd.io/_uploads/ByrR9rRy0.png) EventSource 會先透過客戶端開啓,伺服器就會開始不斷的傳送訊息給客戶端,所以這裡也代表了我們也需要在 js 做一些處理, 這一行就是在講上面的 `new eventsource('some url')`,也就是創建一個新的 EventSource ```cpp= window.addEventListener('load', getReadings); ``` 那這裡的 `getReadings` 又是什麽呢?`getReadings`就是啓動第一次的請求,如下所述: 簡單來説就是將收到的資料要如何進行處理,透過json進行傳輸之後透過parse將資料分割出來 ```cpp= 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 去把資料進行分割 ```cpp= 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 函式,這樣就能達到持續傳送資料的機制了 ```cpp= 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 15. 設定預設 SPIFFS 路徑 16. 設定網頁路徑 17. 設定索取資料路徑 19. 加入 Event Handler 20. 開啓伺服器 22. 處理透過 events 傳送資料 23. 持續監控客戶的連線狀況 這裡一步一步來,先來回顧一下 Arduino 本身的開發架構: ```cpp= #include <library.h> const a; const b; void setup(){ functiona.begin(); functionb.begin(); } void loop(){ //some code; } ``` ### 導入函式庫 我們來想想,如果我們想要把之前的功能全部整合進去的話? 我們需要用的函式庫有哪些呢?大家可以嘗試看看填寫以下的填空哦! ```cpp= #include <Arduino.h> #include <????.h>//跟無線網路有關的 #include <AsyncTCP.h> #include <ESPAsyncWebServer.h> #include "??????.h"//跟資料管理系統有關的 #include <Arduino_JSON.h> #include <DHT.h> ``` ### 建立四大物件 我們要先定義物件,我想大家都很熟悉了 ```cpp= DHT dht(DHT針腳, DHT22); AsyncWebServer server(80); AsyncEventSource events("/events"); JSONVar readings; ``` ### 啓動 WIFI 啓動 WIFI 我們必須先做三個步驟 我們可以多多想一下 以下是提示! 1. 設定 WIFI 模式 2. 開啓 WIFI 3. 等待連接 WIFI 4. 列印現在 IP 到序列埠 ```cpp= WiFi.mode(WiFi_???); WiFi.begin(????, ????); while (WiFi.????() != WL_CONNECTED) { Serial.print('.'); delay(1000); } Serial.println(WiFi.localIP()); ``` ### 開啓 SPIFFS 如果我們想要判別 SPIFFS 有沒有成功裝上去,判別式要如何寫呢? ```cpp= void initSPIFFS() { if (SPIFFS.begin(true) != ???) { Serial.println("An error has occurred while mounting SPIFFS"); } Serial.println("SPIFFS mounted successfully"); } ``` ### 啓動DHT22 ```cpp= dht.begin(); ``` ### 建立JSON化資料的函式 我們剛剛有提過,怎麽把我們想要的資料包裝成 json,以利我們之後進行資料傳輸,所以我們應該在中括號裡寫什麽?最後需要字串化是什麽呢? ```cpp= 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 這裡我們會簡單的給大家一個模板可以直接使用 ```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 ```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 這邊需要你們使用今天教你們的東西了哦!這邊也會給大家提示!把問號的地方填上! ```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路徑 ```cpp= server.serveStatic("/", SPIFFS, "/"); ``` ### 設定網頁路徑 這邊給一些提示 server.on() 的三個變數代表的意思可以回去看一下 ```cpp= // Web Server Root URL server.on("/", ?????, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/index.html", "text/html"); }); ``` ### 設定索取資料路徑 ```cpp= server.on("/readings", ?????, [](AsyncWebServerRequest *request){ String json = getSensorReadings(); // "application" request->send(???, "????", ????); json = String(); }); ``` ### 加入event handler ```cpp= server.addHandler(&events); ``` ### 開啓伺服器 ```cpp= server.begin(); ``` ### 處理透過events傳送資料 ```cpp= void loop() { events.send(getSensorReadings().c_str(),"new_readings" ,millis()); } ```