arduino 官網 arduino uno

Arduino 物聯網應用 - 上課教材

動畫互動網頁程式設計 > 課程內容 > 第 09 章 - 使用 Wifi 模組

第 09 章 - 使用 Wifi 模組

上次更新日期 2022/11/26

相對於藍芽需要有收發設備進行點對點傳輸,目前我們的 wifi 則有很多基地台可以直接應用!因此,使用 wifi 搭配 arduino 似乎也是不錯的選擇。在 arduino nano 33 IoT 上面已經有內建 wifi 模組,我們可以直接呼叫 arduino 官網提供的 wifi 函式庫, 驅動 wifi 硬體之後,就可以進行資料傳輸!甚至於可以在 arduino 上面設定一個小 web server 哩!

學習目標:

  1. 驅動硬體、掃描 wifi 基地台
  2. 連線到 wifi AP 實際連網
  3. 透過 web 進行互動。

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 位址等等。

例題 9.1.1: 讓內建燈號有用途:

修改上面的 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 程式:

簡單的處理流程,請先從 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 的燈號是否正確了!

例題 9.3.1:透過上述的程式碼,另存成新檔 code-wifi-server2,搭配前一章提到的三軸加速器與陀螺儀, 將所有的資料讀出到網站上!最終的結果會有點類似這樣:
$ 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 上取得一些資料,並據以修改自己的動作。

例題 9.4.1: 讓你的 http 服務支援 PHP 的模組
  1. 先安裝 php
  2. 可能需要重新啟動 apache2 服務
  3. 使用 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 的資料庫當中了。

例題 9.4.2:將三軸加速與陀螺儀的數據回傳給 server,請將剛剛的 code-wifi-client1 另存新檔成為 code-wifi-client2 , 然後進行如下的修改:
  • 確認 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 的偵測
將程式碼燒錄時,檢測有沒有問題,有問題再一個一個訂正。然後燒錄後上傳,這樣每隔 5 秒鐘,系統就可以取得來自 arduino 的數據了!這也是一種數據偵測的方式喔!

可以將 arduino 的資料透過 wifi 來進行主動收集或被動處理,實在也是挺有趣的一件事!

9.5: 參考資料