---
# System prepended metadata

title: Final Project-第六周
tags: [NKFW第二届]

---

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