【ESP32專欄】用ESP32做出WiFi即時PM2.5顯示器

作者:尤濬哲

ESP32內建的WiFi模組,因此可以很簡易連上網路,本文將介紹如何上網抓取即時空氣品質PM2.5資料:我們只需要利用WiFi函式庫連上網路,並用HTTPClient模擬一個瀏覽器,就可以上網抓取網頁資料。不過為了讓讀者了解ESP32 WiFi的運作原理,我們會先從WiFiscan這個範例開始講起。

WiFiscan就是掃描網路的意思,就像我們手機開啟WiFi功能時,會先列表附近掃描到的網路名稱及訊號強度,WiFiscan就是這樣的功能,我們掃描到網路之後,再來選擇要使用哪個網路上網。

以下為本文大綱:

  1. WiFiscan網路掃描
  2. HTTPClient抓取PM2.5資訊
  3. JSON資料解析
  4. PM2.5警示器

1. WiFiscan網路掃描

在ESP32中使用無線網路要使用到WiFi.h函式庫內的WiFi物件,另外ESP32啟動WiFi之後,可以選擇四種模式WiFi.mode,列表如下:

製表:尤濬哲

本次將介紹比較常用的WIFI_STA,讓ESP32就像是一台手機,將資料傳到某個資料庫,或者讀取網路的資料。完成設定模式後,就啟動WiFi.scanNetworks()掃描附近的無線網路,除了顯示無線網路的名稱SSID之外,也會顯示訊號強度RSSI,RSSI是負數表示,越接近0代表訊號越強,另外就是有設定密碼的則會標示「*」。

WiFiScan網路掃描是內建的範例程式,我們透過功能表/檔案/範例/WiFi/WiFiScan可以找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "WiFi.h"
void setup(){
Serial.begin(115200);
WiFi.mode(WIFI_STA);//設定為STA工作站模式
WiFi.disconnect();//斷線(初始化的意思)
delay(100);
Serial.println("Setup done");
}
void loop(){
Serial.println("scan start");
int n = WiFi.scanNetworks();//掃描網路,並將掃描到的網路數量存入n
Serial.println("scan done");
if (n == 0) {
Serial.println("no networks found");
} else {
Serial.print(n);
Serial.println(" networks found");
for (int i = 0; i < n; ++i) {
//顯示無線網路SSID, RSSI, 加密
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (");
Serial.print(WiFi.RSSI(i));
Serial.print(")");
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*");
delay(10);
}
}
Serial.println("");
//等候五秒後,重新掃描
delay(5000);
}

本範例關鍵語法包括:

  1. WiFi.mode(WIFI_STA);//設定為STA工作站模式;
  2. WiFi.disconnect();//斷線(作用為初始化的意思)
  3. int n = WiFi.scanNetworks();//掃描網路,並將掃描到的網路數量存入n

筆者掃描結果如下圖,一共掃描到附近的3個網路,前方的英文為該無線網路的名稱,一般稱為SSID(Service Set Identifier:服務識別碼),SSID名稱有分大寫小,後續連線時要注意大小寫是否輸入正確。而後方的數字則是訊號強度RSSI(Received Signal Strength Indicator),以負值表示,越接近0代表訊號越好,一般-50以內代表無線訊號非常好,-110以上則代表訊號非常微弱。

本例所得到的列表中,訊號以1:You(-47)最強、Asus(-82)最弱,而2:You2F後面沒有「*」代表該網路沒有連線密碼。

2. HTTPClient抓取PM2.5資訊

我們將使用ESP32的無線網路讀取公開資訊的PM2.5空氣品質為範例,說明如何讓ESP32抓取網路資料。

上一節掃描網路之後,我們就可以選擇要連上哪一個網路,等連上網路後,就可以使用最簡單的HTTPClient物件讀取網頁資料,所謂的HTTPClient是一個網路物件,可以想像他是一個模擬的瀏覽器,可以讀取網頁的資料,在ESP32中讀取網頁除了HTTPClient外,還有WiFiClient及WiFiClientSecure,功能稍有差異,後續我們用到時會再詳述。

以本例而言,我們將連上最強的網路訊號SSID:You,而密碼則是假設已知為ABCD1234,此時連線整個範例的步驟則為:

  1. 設定WiFi模式:mode(WIFI_STA)
  2. 啟動WiFi連線:begin(ssid, password)
  3. 檢查是否連線成功:status() != WL_CONNECTED
  4. 如果已經連上網路,則啟動網頁連線:begin(“http://PM2.5網址”)
  5. 檢查網頁連線是否正常: httpCode == HTTP_CODE_OK
  6. 如果網頁連線正常,則取得網頁內容:String payload = http.getString()
  7. 將資料顯示在序列監控視窗上

上述完整流程圖如下:

程式碼範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include
#include
char ssid[] = "SSID"; //請修改為自己的SSID
char password[] = "password"; //請修改自己的密碼
char url[] = "http://opendata2.epa.gov.tw/AQI.json"; //PM2.5的網址
void setup() {
Serial.begin(115200);
delay(1000);
Serial.print("開始連線到無線網路SSID:");
Serial.println(ssid);
//1.設定WiFi模式
WiFi.mode(WIFI_STA);
//2.啟動WiFi連線
WiFi.begin(ssid, password);
//3.檢查連線狀態
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.println("連線完成");
}
void loop() {
//4.啟動網頁連線
Serial.print("啟動網頁連線");
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
Serial.print("httpCode=");
Serial.println(httpCode);
//5.檢查網頁連線是否正常
if (httpCode == HTTP_CODE_OK) {
//6.取得網頁內容
String payload = http.getString();
Serial.print("payload=");
//7.將資料顯示在螢幕上
Serial.println(payload);
}
http.end();//關閉網頁連線
delay(10000);
}

下圖為ESP32透過HTTPClient讀取網頁的結果:

除了大部分成功抓取全部的空氣品質資訊回來之外,讀者可以發現其中可能會有多次讀取失敗,而此時回覆的httpcode為-1,推測可能的原因是網路延遲造成。若讀取成功(httpcode=200),則會將該網頁所有內容取回,代表讀取成功,訊息擷取一段如下:

本資料看來都是亂碼,其實是有規則可尋,例如基隆的PM2.5放在上圖紅色標示的位置,而這種格式稱為JSON(JavaScript Object Notation,JavaScript物件表示法),是目前網路最流行的資料交換格式,不過要如何讓ESP32自動找到需要地區的PM2.5的資訊,則必須要透過JSON解析工具。

3. JSON資料解析

JSON格式是一種資料結構,主要為{“欄位1″:”內容1”, “欄位2″:”內容2″….}這樣的方式組成,如以資料庫的觀點來說明,每一個大括弧{ }代表一個紀錄,而內容則依序寫在{ }內部,以分號「:」區隔欄位名稱及內容。舉例來說,以上表取得的資料第一筆「基隆」來說,若要獲得PM2.5的資料,則是尋找”PM2.5”:”19″這個欄位,同理也可以從上表得到桃園市PM2.5為31。

JSON可分成以下兩大種類JSON物件「{ }」與JSON陣列「[ ]」,兩者又可以互相組合,因此結構比資料庫欄位形式較為複雜,先說明兩者的差異。

JSON物件(object):以大括號{ }起訖

每一個欄位都會先標示欄位名稱,在用「:」標示該欄位內容,不同欄位之間用「,」做分隔。

JSON物件是JSON基本形式:JObject={“欄位1″:”內容1”, “欄位2″:”內容2”}

例如我們要表示同學Jake的數學65分,歷史85分,可以這樣寫:{“name”:” Jake “, “Math”:”65″,”History”:”85″}

而多個JSON物件再組成一個JSON物件,例如我們可以把上面的內容改成下面這樣:

JObject={“name”:” Jake “, “grade”:{“Math”:”65″,”History”:”85″}}

也就是說,同學Jake成績單裡面有數學65分,及歷史85分,其意義是相同的,我們只是把兩個成績欄位塞入一個成績單物件中,在成績單物件中分成兩個欄位是數學與歷史。

此時要拿到Jake的數學成績時,我們用JObject[“grade”][“math”]就可以取得數學成績。

如果我們需要再把Mary的成績一起顯示,則必須再多一個JSON物件,因此我們可以改用下面的JSON陣列來表示。

JSON陣列(array):以中括號[ ]起訖

此種就是C語言裡的陣列表示法,因此稱為JSON陣列,其格式大致如下:

[{Json物件1}, { Json物件2}, { Json物件3}, { Json物件4}]

一個JSON陣列是多個JSON物件的組合,每個物件之間以「,」區隔,以上一個例子來說,假設班上有兩位學生,則可用這樣的方式來表示:

JArray= [{“name”:” Jake “, “grade”:{“Math”:”65″,”History”:”85″} , {“name”:” Mary “, “grade”:{“Math”:”84″,”History”:”32″}}]

此時要拿到Jake的數學成績時,就用JArray [0][“math”],若是要Mary的歷史成績則是JArray [1][“History”],我們可以發現Array與Object取值時Array用的是[數字],代表第幾個元素,而Object則是用[“欄位”]。

而空氣品質網站提供的內容為中括號[ ]開頭及結尾,因此我們將用會Array的方式進行解析。

了解格式後,我們在Arduino IDE中安裝AduinoJson函式庫以解析資料內容,如下圖:首先點選功能表/草稿碼/匯入函式庫/管理函式庫,在跳出的程式庫管理元中,輸入關鍵字JSON即會出現我們需要的函式庫AduinoJson,請注意要認清作者為Benoit Blanchon的這一個函式庫。

完成安裝後,我們可以將程式上方引用ArdunioJson函式庫,並對取得的網頁內容Payload進行JSON解析,其解析的語法為:

1
2
3
4
5
6
7
8
    //假設payload是空氣品質JSON陣列
//要一個payload大小x2的JSON暫存
DynamicJsonDocument AQJarray(payload.length()*2);
deserializeJson(AQJarray, payload);//解析payload為JSON Array格式
String KLPM25=AQJarray[0]["PM2.5"];//第0個是基隆的PM2.5
String XHPM25=AQJarray[1]["PM2.5"];//第1個是汐止的PM2.5
Serial.println("基隆PM2.5:" + KLPM25);
Serial.println("汐止PM2.5:" + XHPM25);

結果如下圖:

再介紹幾個JSON語法:

DynamicJsonDocument Json變數(size);

是向系統要一個名為「Json變數」的空間存放JSON,至於空間要多大,比較保險的作法是字串長度x2,本例選擇payload.length()*2。

deserializeJson(Json變數,待解析之JSON字串);

是將「待解析之JSON字串」完成解析後,將結果放在「Json變數」,本例「待解析之JSON字串」就是自網路取得的空氣品質字串payload,完成解析後的Json變數為AQJarray

String KeelungPM25=AQJarray[0][“PM2.5”];

此時需要任何資料,就可以透過Json變數[序號][“欄位名”]取得相關的空氣品質數值。

不過呢,用陣列序號的方式來找PM2.5資訊,前面幾筆還比較好算,如果您的地區剛好中間或尾巴,那不知道要算多久?這時我們可以用For或While迴圈來協助我們比較,例如您的地區是高雄市橋頭,已知橋頭的SiteId站台編號為48,此時我們用For迴圈來找SiteId=48即可。

讀者也許會問,為何不直接比較站台名SiteName=橋頭呢?因為網路上中文的比對比較容易出問題,筆者曾經遇到過同樣是”橋頭”卻不等於”橋頭”,看起來一樣,但背後的編碼不同導致的比對問題,因此使用編號的比對是比較正式作法的。

可參考下圖:

進一步來看看程式碼:

1
2
3
4
5
6
7
8
for (int i = 0; i < AQJarray.size(); i++) {
if (AQJarray[i]["SiteId"] == "48") {
//橋頭站SiteId為"48"
String QTPM25 = AQJarray[i]["PM2.5"];
Serial.println("橋頭PM2.5:" + QTPM25);
break;//退出for迴圈
}
}

程式碼中:

for (int i = 0 ; i < AQJarray.size() ; i++)

是比對迴圈的初始設定,從第0個開始,比對到最後一個,如何知道最後一個呢?AQJarray.size()代表這列的總數量:

if (AQJarray[i][“SiteId”] == “48”)

比較第i個元素的SiteId是否為橋頭站的編號”48″,如果是的話,就取得該站的PM2.5資訊,顯示在序列監控視窗中。

而最後的break;則是退出For迴圈,因為我們要的橋頭站資訊已經取得,不需要再比對到最後一站。

再來看看結果:

本段完整的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include
#include
char ssid[] = "SSID"; //請修改為自己的SSID
char password[] = "password"; //請修改自己的密碼
char url[] = "http://opendata2.epa.gov.tw/AQI.json"; //PM2.5的網址
void setup() {
Serial.begin(115200);
delay(1000);
Serial.print("開始連線到無線網路SSID:");
Serial.println(ssid);
//1.設定WiFi模式
WiFi.mode(WIFI_STA);
//2.啟動WiFi連線
WiFi.begin(ssid, password);
//3.檢查連線狀態
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
Serial.println("連線完成");
}
void loop() {
//4.啟動網頁連線
Serial.print("啟動網頁連線");
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
Serial.print("httpCode=");
Serial.println(httpCode);
//5.檢查網頁連線是否正常
if (httpCode == HTTP_CODE_OK) {
//6.取得網頁內容
String payload = http.getString();
Serial.print("payload=");
//7.將資料顯示在螢幕上
Serial.println(payload);
DynamicJsonDocument AQJarray(payload.length() * 2);
deserializeJson(AQJarray, payload);//解析payload為JSON Array格式
String KLPM25 = AQJarray[0]["PM2.5"]; //第0個是基隆的PM2.5
String XHPM25 = AQJarray[1]["PM2.5"]; //第1個是汐止的PM2.5
Serial.println("基隆PM2.5:" + KLPM25);
Serial.println("汐止PM2.5:" + XHPM25;
for (int i = 0; i &lt; AQJarray.size(); i++) {
if (AQJarray[i]["SiteId"] == "48") {
//橋頭站SiteId為"48"
String QTPM25 = AQJarray[i]["PM2.5"];
Serial.println("橋頭PM2.5:" + QTPM25);
break;//退出for迴圈
}
}
http.end();//關閉網頁連線
delay(10000);
}

4. PM2.5警示器

最後我們利用之前學過得LED燈來顯示現在的空氣品質狀態,假設我們學校規定空氣品質的指標有三級,分別為優良、普通、危害,其數值如下:

  • 優良:PM2.5<=15,顯示綠燈
  • 普通:PM2.5<=30,顯示黃燈
  • 危害:PM2.5>30,顯示紅燈

假設學校規定當空氣品質呈現第三級「危害」時,暫停戶外活動,並顯示紅燈提醒同學,因此我們需要在程式中加入比較。

理論上,我們只需要把QTPM25拿來比較即可,但QTPM25資料型態是字串,無法直接進行數字運算,此時我們要先將QTPM25型別轉換。程式碼如下:

1
2
3
4
5
6
7
8
int QTPM25Value= QTPM25.toInt();//將字串轉整數
if(QTPM25Value &lt;15){
//空氣品質程度&lt;15,屬於優良 digitalWrite(15,HIGH);//亮綠燈 digitalWrite(2,LOW); digitalWrite(4,LOW); } if(QTPM25Value &gt;=15 &amp;&amp; QTPM25Value &lt;30){ //空氣品質程度15~30之間,屬於普通 digitalWrite(15,LOW); digitalWrite(2,HIGH);//亮黃燈 digitalWrite(4,LOW); } if(QTPM25Value &gt;=30){
//空氣品質程度&gt;30,屬於危害
digitalWrite(15,LOW);
digitalWrite(2,LOW);
digitalWrite(4,HIGH);//亮紅燈
}

接線圖如下:

(作者為本刊共筆作者,其專欄文章同步發表於作者部落格原文連結;責任編輯:歐敏銓)

尤濬哲

Author: 尤濬哲

身兼助理教授/專欄作家/知名部落客,以及點點滴滴科技研發總監等身份,專長包括人工智慧、多媒體互動(Unity)、智慧互動裝置(APP、Arduino)、虛擬實境與擴增實境互動、IoT 實做開發。 學歷:中山大學資訊管理研究所 博士

Share This Post On
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x