# 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 的運作原理:

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());
}
```