第 09 章 - 使用 Wifi 模組
上次更新日期 2022/11/26
相對於藍芽需要有收發設備進行點對點傳輸,目前我們的 wifi 則有很多基地台可以直接應用!因此,使用 wifi 搭配 arduino 似乎也是不錯的選擇。在 arduino nano 33 IoT 上面已經有內建 wifi 模組,我們可以直接呼叫 arduino 官網提供的 wifi 函式庫, 驅動 wifi 硬體之後,就可以進行資料傳輸!甚至於可以在 arduino 上面設定一個小 web server 哩!
學習目標:
- 驅動硬體、掃描 wifi 基地台
- 連線到 wifi AP 實際連網
- 透過 web 進行互動。
- 9.1: 應用 arduino wifi 函式庫進行基地台掃描與連線
- 9.2: 使用非官網提供 NTPClient 更新時間
- 9.3: 編寫 web server 與互動功能
- 9.4: 讓 arduino 主動傳資料到 webserver 上
- 9.5: 參考資料
9.1: 應用 arduino wifi 函式庫進行基地台掃描與連線
前一章我們使用了 arduino nano IoT 的藍芽模組,這一章我們則要來使用這塊開發板的 wifi 模組。arduino nano IoT 已經內建 wifi 硬體, 所以,應用上,我們只要安裝好可以驅動這個硬體的函式庫,理論上,就可以喚醒 wifi 無線網路了。現在,開啟 IDE 環境, 從『草稿碼』、『匯入程式庫』、『管理程式庫』,輸入 wifinina 搜尋,出現的第一個函式庫安裝起來就對了。
- 掃描環境中的 wifi AP
用過 wifi 基地台的都知道,我們得要曉得基地台的 SSID 以及密碼才行!那如何知道環境中有哪些 wifi 基地台的 SSID 呢? WIFININA 模組有提供一個簡單的掃描範例程式,直接抓下來編譯上傳即可!打開『檔案』、『範例』、『WifiNINA』、『ScanNetworks』程式, 裡面任何資料都不要改它,直接編譯後上傳,然後打開終端界面,就應該會看到如下的畫面在終端機上:
Scanning available networks... ** Scan Networks ** number of available networks:10 0) RPi-vbird Signal: -47 dBm Encryption: WPA 1) vbird-2.4G Signal: -50 dBm Encryption: WPA2 2) i2504_2.4G Signal: -80 dBm Encryption: WPA2 ....
終端機當中會顯示目前的 SSID 名稱以及連線加密的機制為何。以上面來說,第 0 台使用的是 WPA 加密機制,其他兩部基地台則是使用 WPA2 的機制這樣。根據不同的加密機制,等等我們選擇的連線範例檔也會不一樣喔!
- 嘗試連線到某一台 wifi AP 上
假設如上面所示,你已經知道你要連線到 RPi-vbird 這部 wifi AP 上,而且也假設你知道該 AP 的密碼了,這時,你就可以打開『檔案』、 『範例』、『WiFiNINA』、『ConnectWithWPA』程式碼,先將該程式另存新檔成為 code-wifi-connect 檔案,然後修改一下裡面的資料, 因為輸出的訊息我們想要做個簡單的處理,所以底下的程式碼會有點不同!包括還有設定了 DNS IP 位址!如下所示:
#include <SPI.h>
#include <WiFiNINA.h>
char ssid[] = "RPi-vbird"; // 改成你的 AP 名稱
char pass[] = "xxxxxxxxx"; // 改成你的 AP 登入號碼
IPAddress dns1(120,114,100, 1); // first DNS server
IPAddress dns2( 8, 8, 8, 8); // second DNS server
int status = WL_IDLE_STATUS; // the WiFi radio's status
void setup() {
Serial.begin(9600);
// 準備連上 AP
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
delay(1000);
}
// 連上 AP 之後才開始設定好 DNS
Serial.print("You're connected to the network");
WiFi.setDNS(dns1, dns2);
////////// 在這之上的程式碼,就可以連上網路了!底下是為了訊息輸出而已!
printWifiData();
}
void loop() {
// 每五秒輸出一次訊息
delay(5000);
printCurrentNet();
}
void printWifiData() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print the MAC address of the router you're attached to:
byte bssid[6];
WiFi.BSSID(bssid);
Serial.print("WiFi AP's MAC: ");
printMacAddress(bssid);
// print your arduino MAC
byte mac[6];
WiFi.macAddress(mac);
Serial.print("Arduino's MAC: ");
printMacAddress(mac);
}
void printCurrentNet() {
// print your board's IP address:
IPAddress ip1 = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip1);
IPAddress ip2 = WiFi.gatewayIP();
Serial.print("Gateway IP: ");
Serial.println(ip2);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.print(rssi);
Serial.println("dBm");
}
void printMacAddress(byte mac[]) {
for (int i = 5; i >= 0; i--) {
if (mac[i] < 16) {Serial.print("0"); }
Serial.print(mac[i], HEX);
if (i > 0) { Serial.print(":");}
}
Serial.println();
}
燒錄後上傳,如果一切順利,可以連接上 Wifi AP 的話,就會出現類似底下的資料了:
Attempting to connect to WPA SSID: RPi-vbird You're connected to the networkSSID: RPi-vbird WiFi AP's MAC: B8:27:EB:D8:AE:89 Arduino's MAC: 4C:EB:D6:4D:61:F8 IP Address: 192.168.33.187 Gateway IP: 192.168.33.254 signal strength (RSSI):-52dBm
這樣就連上網路,而且還能夠看到詳細的資訊了!包括路由器的 IP 以及我們自己 arduino 的 IP 位址等等。
修改上面的 code-wifi-connect 程式,增加 pinMode 與 digitalWrite 功能,使用內建的 LED (LED_BUILTIN) 燈號, 讓 arduino 連上網路後,就讓燈號亮起來!讓我們可以簡單了解該 arduino 有沒有成功連上網路。設計上面相當簡單, 都需要在 setup 函數裡面處理!只要在連上 AP 的位置前後放置好寫入 LOW 與 HIGH 的電位,就可以讓內建的 LED 燈號啟動/關閉了!
9.2: 使用非官網提供 NTPClient 更新時間
arduino 上面因為缺乏電池記憶,因此 arduino 預設是沒有什麼時間概念的!時間都是從 1970/01/01 開始計算的日期, 並不是實際的日期。現在既然有 wifi 了,我們當然可以去更新系統的時間參數了。你可以從『檔案』、『範例』、『WiFiNINA』、 『WiFiUdpNtpClient』範例程式,修改該檔案裡面的 SSID、PASS、timeServer 的 IP 位址,燒錄上傳後, 就可以校正你的 arduino 時間了!不過,該程式碼輸出的資訊比較陽春,我們可以使用另一個模組來處理這個時間校正的任務! 不過有太多的 NTPClient 模組,鳥哥想要找到比較多資訊功能的程式碼,官方提供的資料較少 (應該是考慮到記憶體使用情況), 所以,鳥哥找的是底下的 NTPClient 程式:
- NTPClient: https://github.com/taranais/NTPClient
- 安裝使用介紹: https://randomnerdtutorials.com/esp32-ntp-client-date-time-arduino-ide/
- 直接下載:https://github.com/taranais/NTPClient/archive/refs/heads/master.zip
- 最終你 windows 目錄名稱: C:\Users\你帳號\Documents\Arduino\libraries\NTPClient
簡單的處理流程,請先從 github 上面,按下『 Code 』按鈕,選擇『Download ZIP』,或者從上面直接下載的連結直接下載, 下載完畢後,解開檔案,會得到名為 NTPClient-master 的目錄,將此目錄複製到你的 arduino library 目錄中, 以鳥哥為例,我的目錄會是在:『 C:\Users\dmtsai\Documents\Arduino\libraries 』這樣。 於是就多了如上所示的新增的名稱。
- 更新 arduino 上面的日期與時間
預設的情況底下,這個 NTPClient 會主動去找 pool.ntp.org 的時間伺服器群,如果你是在台灣地區,那也可以使用 tw.pool.ntp.org, 不過,在某些特殊狀況,例如鳥哥所在的測試區域,它只提供了特定的 IP 位址可以連上 NTP 伺服器時,那就得要使用特別的主機名稱了。 這一版比較特別,它不能支援 IP 位址,只支援主機名稱!所以,你得要在有網路,且有設定好 DNS 的情況下,才有辦法連上來喔! 假設底下的程式碼被稱為 code-wifi-NTP 好了:
#include <SPI.h>
#include <WiFiNINA.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
WiFiUDP ntpUDP;
char ssid[] = "RPi-vbird"; // your network SSID (name)
char pass[] = "xxxxxxxxx"; // your network password
IPAddress dns1(120,114,100, 1); // first DNS server
IPAddress dns2( 8, 8, 8, 8); // second DNS server
char ntpserver[] = "ntp.ksu.edu.tw"; // NTP 伺服器
int GMT = +8;
int status = WL_IDLE_STATUS; // the WiFi radio's status
NTPClient ntpClient(ntpUDP, ntpserver, 3600*GMT);
void setup() {
Serial.begin(9600);
// 準備連上 AP
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
delay(1000);
}
// 連上 AP 之後才開始設定好 DNS
Serial.println("You're connected to the network");
WiFi.setDNS(dns1, dns2);
ntpClient.begin();
ntpClient.update();
}
void loop() {
// 每秒輸出一次訊息
Serial.println(ntpClient.getFormattedDate());
Serial.println(ntpClient.getFormattedTime());
Serial.println(ntpClient.getHours());
Serial.println(ntpClient.getMinutes());
Serial.println(ntpClient.getSeconds());
delay(1000);
}
編譯上傳完畢之後,你的 arduino 應該會有正確的時間了!上面範本中的 getHours 等,只是讓你知道, 可以用這個方式去處理取得小時、分鐘等數據!有助於你設計例如電子時鐘等任務喔!
9.3: 編寫 web server 與互動功能
現在,讓我們準備來進行透過網頁網址功能來進行資料的傳輸吧!我們想要透過 WiFiNINA 模組的 WiFiServer 與 WiFiClient 偵測有沒有人連上我們 arduino 的 port 80 ,來進一步設計輸出資料的功能!然後,當首次連線到我們的系統時, 就單純顯示網頁,同時提供兩個連結,分別是:
- http://your.ip/HI
- http://your.ip/LO
當 arduino 發現使用者輸入的資訊有 /HI 這個關鍵字,就會點亮 LED 燈,如果輸入 /LO 就會關閉 LED 燈。 同時網頁也會提供目前的燈號資訊。
- 配置 LED 燈
首先,我們先將 LED 的正極連接到 D5 的腳位上,然後負極連接到接地。這樣就可以展示 LED 燈了!
- 編輯 code-wifi-server1 檔案
請先將 code-wifi-ntp 這個檔案另存新檔成為 code-wifi-server1,然後主要修改 code-wifi-server1 的內容。 首先,在全域變數的地方,增加底下這串資料:
//////////////////// Wifi server WiFiServer server(80); // 建立名為 server 的伺服器物件 WiFiClient client = server.available(); // 建立名為 client 的用戶端展示物件 int led = 5; // LED 燈號的腳位 String ledstat = "Off"; // 預設的燈號狀態為 off 的! ////////////////////
接下來,在 setup 的函數當中 (你可以在最下方加入),增加啟動 server 以及設定好 led 腳位成為 OUTPUT 方式:
//////////////////////// server.begin(); pinMode(led,OUTPUT); ////////////////////////
在 loop 函數中,確認一下 server 是不是有人來使用?如果有人來使用,我們才喚醒 arduino 的網頁輸出功能! 否則 arduino 不會有任何動作才對!簡單的寫法,有點像這樣:
void loop() { client = server.available(); if ( client ) { Serial.println(ntpClient.getFormattedDate()); printWEB(); } }
最後,當然就是編寫 printWEB() 函數,這個檔案的內容會有點像底下這樣:
void printWEB() { // 先顯示一些簡單的資訊,通知用戶端連線來,而且設計取得的資訊為空白 Serial.println("new client link"); String currentLine = ""; // 當用戶端確實連上來之後,才進行資料 while ( client.connected() ) { // 確定用戶端的內容是可讀取的 if ( client.available() ) { // 設定 msg 為用戶端傳來的資訊 (例如網址列資訊等) char msg = client.read(); Serial.write(msg); // 如果 msg 僅是簡單的空白行, if ( msg == '\n' ) { // 開始印出網頁內容 if ( currentLine.length() == 0 ) { client.println("HTTP/1.1 200 OK"); client.println("Content-type:text/html"); client.println(); client.print("Now time is: "); client.println(ntpClient.getFormattedDate()); client.println("<br />"); client.print("LED status is: "); client.println(ledstat); client.println("<br />"); client.println("Click <a href=\"/HI\">here</a> turn the LED on<br>"); client.println("Click <a href=\"/LO\">here</a> turn the LED off<br><br>"); client.println(); break; } else { currentLine = ""; } // 如果有資料,這才開始累加目前的行 } else if ( msg != '\r' ) { currentLine += msg; } // 如果目前的行裡面有 /HI 的網址,就給予 LED 亮 if ( currentLine.endsWith("GET /HI") ) { Serial.println("turn on"); digitalWrite(led, HIGH); ledstat = "On"; } // 如果目前的行裡面有 /LO 的網址,就給予 LED 暗 if ( currentLine.endsWith("GET /LO") ) { Serial.println("turn off"); digitalWrite(led, LOW); ledstat = "Off"; } } } client.stop(); Serial.println("client disconnected"); }
接下來,將程式碼編譯上傳,等到 arduino 的內建 LED 燈亮 (代表連上 AP 了),我們再到樹莓派上面, 直接執行底下的指令來查看結果:
$ curl http://192.168.33.187/ Now time is: 2022-11-25T12:00:52Z <br /> LED status is: Off <br /> Click <a href="/HI">here</a> turn the LED on<br> Click <a href="/LO">here</a> turn the LED off<br><br> $ curl http://192.168.33.187/HI $ curl http://192.168.33.187/LO
依序執行看看,就可以知道 LED 的燈號是否正確了!
$ curl http://192.168.33.187/
Now time is: 2022-11-25T13:53:52Z
<br />
Accelerometer sample rate = 104.00 Hz
Acceleration in g's
X Y Z
0.29 0.60 0.93
Gyroscope sample rate = 104.00 Hz
Gyroscope in degrees/second
X Y Z
0.85 25.51 -24.60
透過上述的功能,我們就可以使用網址列的相關資料,來與 arduino 進行互動了!相當簡單!
9.4: 讓 arduino 主動傳資料到 webserver 上
上一小節主要討論的是,我們從外面連線到 arduino 模擬出來的 web server,那如果是 arduino 主動要傳送資料給 web server 呢? 也就是,如何讓 arduino 模擬成為一個瀏覽器的類似的意思。其實也不是太困難,透過 WiFiNINA 的 WiFiClient 模組, 使用 client.println("GET /url") 的方法與 client.read() 的方法,就可以當作是瀏覽器用戶端的功能了! 只是,前提是,你得要有額外的 web server 才行!
- 使用樹莓派指定 LED 開關功能
假設你使用了樹莓派,而且也裝好了 web 服務,假設安裝的是 apache 軟體的話,那麼 /var/www/html 就會是你的首頁目錄!假設在 /var/www/html/led 這個檔案內容當中,設定了一行,如果是 ON 那麼就啟動 LED 亮燈, 如果是 OFF 就關閉 arduino 上面的 LED 燈。現在,讓我們先來製 led 這個檔案內容:
$ sudo vim /var/www/html/led
ON
基本上, arduino 瀏覽器並不是這樣用的!一般來說,都是在樹莓派或數據中心先寫好類似 PHP 的 API 程式, 然後透過類似『 http://your.server/api.php?name=who&work=haha 』的方式,傳輸變數與其內容給 web server 使用。 不過,我們這裡就簡單使用一個方式來處理而已!確認兩者可以溝通就好了!
- 設定 arduino 的程式
接下來,將剛剛的 code-wifi-server2 另存新檔成為 code-wifi-client1,並進行一些修改。 在全域變數的地方,我們需要設定 Server 的 IP 位址,同時指定 LED 燈號所在的腳位,設計上面可以是這樣:
//////////////////// IPAddress rserver(192,168,33,254); WiFiClient client; int led = 5; ////////////////////
注意喔, IP 位址中間填寫的是你能夠管理的伺服器 IP,同時 IP 當中的符號是逗號 (,) 不是句號 (.), 這很容易搞錯的。之後到 setup 的函數內增加 led 的腳位成為輸出的模式:
/////////////////////////// pinMode(led,OUTPUT); ///////////////////////////
loop 的函數比較有趣,我們指定一個字串 (String) 變數,這個變數會回傳來自 server 的網址內容, 所以,第一段 loop 內容,會有點像這樣:
String msg; if ( client.connect(rserver, 80) ) { Serial.println("connect to server"); client.println("GET /led"); client.println(); delay(5000); // 等待資料處理完畢上傳的時間 }
我們會持續判斷 server 的資料,有點類似一直傳送數據給 server 的感覺。因此,就需要等待一段 server 回傳數據的時間 (delay)。 等到回傳完畢之後,就可以將資料讀入成為字串了:
Serial.println("print content of web page"); while (client.available()) { msg = client.readString(); // 用字串方式處理 msg.trim(); }
最後,資料讀完後,連線就中斷,此時開始判斷數據內容,有出現大寫 ON 就打開 LED 燈,有出現 OFF 就關閉 LED 燈, 不然就不動作。結果會有點像這樣:
if (!client.connected()) { Serial.println("-------------------------"); Serial.println(msg); Serial.println("disconnecting from server."); client.stop(); // 開始處置 LED 的開或關 if ( msg.endsWith("ON") ) { Serial.println("turn on LED"); digitalWrite(led,HIGH); } if ( msg.endsWith("OFF") ) { Serial.println("turn OFF LED"); digitalWrite(led,LOW); } }
處理完畢後編譯上傳,那每 5 秒鐘,arduino 就會主動從 server 上取得一些資料,並據以修改自己的動作。
- 先安裝 php
- 可能需要重新啟動 apache2 服務
- 使用 sudo apachectl -M | grep -i php 確認有沒有出現 php 模組,即可判斷 apache 是否支援 PHP 模組了。
如上,現在你的樹莓派已經支援 PHP 了,簡單的來說,你現在已經可以透過簡單的方式,將資料上傳到樹莓派上面了! 例如,底下分別取得 6 個數據,分別是三軸加速的 X, Y, Z 以及陀螺儀的 X, Y, Z 三部份:
$ sudo vim /var/www/html/getxyz.php <?php $x1 = $_REQUEST['x1']; $y1 = $_REQUEST['y1']; $z1 = $_REQUEST['z1']; $x2 = $_REQUEST['x2']; $y2 = $_REQUEST['y2']; $z2 = $_REQUEST['z2']; echo "三軸加速: X=". $x1 . ", Y=" . $y1 . ", Z=" . $z1 . "<br />\n"; echo "陀螺儀: X=". $x2 . ", Y=" . $y2 . ", Z=" . $z2 . "<br />\n"; ?> $ curl "http://localhost/getxyz.php?x1=0.1&y1=0.2&z1=0.3&x2=1.1&y2=1.2&z2=1.3" 三軸加速: X=0.1, Y=0.2, Z=0.3<br /> 陀螺儀: X=1.1, Y=1.2, Z=1.3<br />
如上所示,該網址就可以將數據傳送到 web server 上面,web server 就可以將該數據填入類似 SQL 的資料庫當中了。
- 確認 Arduino_LSM6DS3.h 已經被匯入
- 全域變數當中,增加了 xx1, yy1, zz1, xx2, ty2, zz2 的浮點數宣告
- 在 setup 函數當中,確認啟動了 IMU 功能
- 在 loop 函數中,先加入底下這段,來取得數據,並加入成為網址列的內容:
if (IMU.accelerationAvailable()) { IMU.readAcceleration(xx1, yy1, zz1); } if (IMU.gyroscopeAvailable()) { IMU.readGyroscope(xx2, yy2, zz2); } String url = "GET /getxyz.php"; url = url + "?x1=" + String(xx1, 2); url = url + "&y1=" + String(yy1, 2); url = url + "&z1=" + String(zz1, 2); url = url + "&x2=" + String(xx2, 2); url = url + "&y2=" + String(yy2, 2); url = url + "&z2=" + String(zz2, 2);
- 將 client.println 的函數中,使用 url 取代該內容
- 取消 LED 的偵測
可以將 arduino 的資料透過 wifi 來進行主動收集或被動處理,實在也是挺有趣的一件事!
9.5: 參考資料
- 非常完整的 arduino nano 33 IoT 介紹與實務,建議可以花時間好好看一下:
https://dronebotworkshop.com/arduino-nano-33-iot/ - arduino 的 WiFiNINA 官方文件:
https://www.arduino.cc/reference/en/libraries/wifinina/ - 相對容易,而且可以取得較多時間參數的 NTPClient github 網站:
https://github.com/taranais/NTPClient - 台灣的 NTP 伺服器:
https://www.pool.ntp.org/zone/tw - 設計 web server 搭配燈號處理:
https://docs.arduino.cc/tutorials/uno-wifi-rev2/uno-wifi-r2-hosting-a-webserver