替 Arduino Nano 開無線外掛 (二) 使用 WiFiEsp 函式庫

STM32F103C8T6 + ESP-01
之前曾經在《替 Arduino Nano 開無線外掛 (一) 接一片ESP-01》一文中,介紹用 Arduino Nano 連接 ESP-01 達到與外界通訊的目的。不過該文當時還沒寫完,只有提到 Arduino 跟 ESP-01 兩者之間的連接、通訊。事實上,這樣的動作還沒有到達 Internet,還只是板子之間的通訊,簡單測試一下 ESP-01 的 AT指令功能。除非,你在終端機上,用一個一個 AT 指令,逐步下給 ESP-01,讓 ESP-01 執行以下動作:1. 設定WiFi模式 (Station, Server等) ;2. 連接無線AP,獲得 IP;3. 設定通信協定、連接遠端伺服器或等待客戶端連進來;4. 傳送字串資料給遠端的伺服器或接收客戶端來的資料。

這樣才完成開無線外掛的目的。不過,總不能每次都這樣子敲一大堆 AT 指令吧?本文接續前文,繼續介紹完整的外掛控制方法,包含讓你在遠端執行一個 UDP 伺服器,實際收取 Arduino Nano 這端的資料。

然後,你可以蒐集感測器的資料、控制、或執行大量的運算。比如,在伺服器這頭,蒐集來自Arduino 這頭的大數據,並執行 Deep Learning 訓練,同時也可以即時下命令給 Arduino 這端。

連接 ESP-01,相關硬體設定與注意事項

前文《接一片ESP-01》已經介紹過硬體的連接方式,這邊就不再贅述。由於通訊過程中,想要知道通訊的狀況,因此本文還是以上次的方法,開兩個 Serial,其中一個用 Software Serial 代替。需要注意的有以下幾點:

  1. ESP-01 的電源跟訊號是 3.3V ,要小心
  2. ESP-01 有時瞬間電流可能到 600mA,放一個電容比較穩定
  3. SoftwareSerial 的速度上不了 115200,需要先用 USB-TTL,直接連接ESP-01,用AT指令:

AT+UART_DEF=57600,8,1,0,0 

先把 ESP-01 的鮑率設成57600 bps。

或許你已經眼尖發現,本文的照片是 STM32了,而非 Arduino Nano。STM32 有三個硬體的串列埠,全部都可以用 115200 bps ,而且STM32本身的信號就是 3.3V,包含L298N馬達驅動器、SR-04超音波感測器、陀螺儀,整組走 3.3V ,會更方便,而且 STM32可以喂 3.3V、5V。

不過照片上電源模組上的 AMS1117 5V 穩壓IC,用一陣子就超燙,之後開始不穩定,電力不足。最好還是買一片 Switch的降壓模組,電力夠也不會發燙。像以下照片這種降壓模組,背面有幾個電壓焊點選擇,用銲錫接起來,就可以直接輸出固定的電壓,很方便。

迷你降壓模組

3A 迷你降壓模組

用AT指令傳送資料給 UDP 伺服器

如果按照之前的介紹,你可以在終端機下(Serial Monitor),慢慢一行一行敲下一連串AT指令,完成以下的動作:
  1. 將 ESP-01 設成 SoftAP + Station Mode
  2. 連接 Wifi 無線分享器,輸入分享器的 SSID 跟密碼
  3. 驗證配發的 IP
  4. 設定多方連接 (AT+CIPMUX=1)
  5. 啟動本地 UDP 通信協定 (192.168.11.169),用 Link ID 3 (因為之前設定多方連接,可以從0-4隨便選一個,如果真的要多方接,那就一個遠方IP固定用一個號碼,最多連接五個地方)
  6. 連線遠方UDP伺服器 (192.168.11.50),準備傳送資料。將本地方跟遠方的 Port,都設定成 10002 (兩邊的Port可以不一樣),最後一個2,代表建立後,遠端的UDP對象可以變。等等下 AT+CIPSEND 加上對方的 IP,可以改,一對多UDP Server的狀況下可以這樣用。
  7. 用3號LinkID,開始傳送 7個 byte 位元組的資料
  8. 輸入7個位元組'abcdefg',如果你先按前四個字元(每個字元1個byte),ESP-01會繼續等你敲入剩下3個字元。
整個過程像以上這樣。你可以按照 Arduino Nano 開無線外掛 (一) 裡面的那個範例程式,在Arduino IDE 的 Serial Monitor 裡面,逐一輸入以下 AT 指令。記得要把 Serial Monitor 視窗下方的換行方式,改成 "Both NL & CR",ESP-01才看得懂你輸入的 AT 指令。

AT+CWMODE=3
OK
AT+CWJAP="SSID_ASUS","password"
WIFI CONNECTED
WIFI GOT IP
OK
AT+CIFSR
+CIFSR:APIP,"192.168.4.1"
+CIFSR:APMAC,"5e:cf:7f:1a:5c:13"
+CIFSR:STAIP,"192.168.11.169"
+CIFSR:STAMAC,"5c:cf:7f:1a:5c:13"
OK
AT+CIPMUX=1
OK
AT+CIPSTART=3,"UDP","192.168.11.50",10002,10002,2
3,CONNECT
OK
AT+CIPSEND=3,7,"192.168.11.50",10002
OK
> (輸入abcdefg)
busy s...
Recv 7 bytes
SEND OK

使用 WiFiEsp 第三方函式庫

像上面那樣自己下 AT 指令,透過 Software Serial 指令去給 ESP-01執行,還挺麻煩的。不過剛開始接觸時,你可以從樂鑫官方資料, 《ESP8266 AT 指令使⽤示例》學會用AT指令來控制 ESP-01的基本觀念,對以後使用 ESP-01,也很有幫助。

好在 Bruno (bportaluri) 已經仿造 Arduino 官方 WiFi 函式庫,寫了一個叫做 WiFiEsp 的函式庫,用簡單的指令,連上 Internet、傳送資料。這個函式庫,可以在 Arduino IDE 的 Manage Libraries 選單裡面找到,安裝以後就可以用了。

你如果跟我一樣,是用 STM32F103C8T6 藍色小藥丸,那需要參考一下這篇的介紹,改動一些原始碼,才能正確編譯。

下面給你一個參考程式碼。這個程式,是用 Arduino Uno、Arduino Nano 當目標板卡,如果要用 STM32,只要將 SoftwareSerial ESPSerial,改成 Serial2 就能用了。然後把ESP-01的Rx Tx 接到Blue Pill 的 Tx2 Rx2。然後,鮑率可以用ESP-01預設的 115200 bps,不用改成 57600 bps。

網路上大部分的教學,都是示範傳送明碼字串。而這個程式,展示傳送完整的結構資料(結構裡面有字元、Bool、整數、浮點數),一整包資料以Byte串不斷傳過去 UDP Server。這樣效率高、不會浪費頻寬。其他有的教學,則是在ESP-01這頭,用HTTP Server方式,服務遠端來的網址。看起來易懂,但多半示範開關LED,低速讀溫度資料玩玩,不是很實用。也有採用MQTT訊息中介的架構。主要用在 Datalog 應用,比如蒐集果園某處的溫度、魚塭某個角落的pH值、PM2.5之類訊息中介,也沒辦法做到即時控制。即時控制的應用,建議建構在 UDP 通訊協定,加上編碼後的位元組串傳送,才比較有效率。

這個範例程式使用的時候,要自己去改那些網路 IP、Port、SSID、Passwod的資料,並注意防火牆的設定,無線分享器不要把你設定的那個 Port 擋住了。這個範例,我是使用 10002  Port,並已經事先在UDP Server主機的防火牆中,打開這個 Port。 如果你的伺服器端或無線分享器裡面,也有啟用防火牆,也記得先去打開。ESP-01本地端,本來就沒有防火牆,無須設定。

不要被程式的長度嚇到,會寫這麼長,只是模擬自走車,不斷送資料給 UDP server 的狀況,並印很多說明資料到 Serial Monitor,了解每一個步驟的狀況。把 Arduino 的 USB 接上電腦,然後在 Arduion IDE,選擇正確的USB Port,打開Serial Monitor,就可以看程式運作的過程。

以下我盡量把程式中的註解寫清楚一點,應該不會太難看懂。

參考程式碼 esp8266wifiudp.ino :


#include <SoftwareSerial.h>

#include "WiFiEsp.h"

#include "WiFiEspUdp.h"


SoftwareSerial ESPSerial(2, 3); // 開一個 Software Serial,RX, TX

//模擬編號三種自走車運動模式,分別是 1, 2, 3

enum drv_mode {

  MOVE_STRAIGHT,

  MOVE_ROTATE,

  MOVE_TURN

};


//用來傳送封包資料的結構。稍後可以把一坨資料傳過去。傳過去之前,要先把結構轉成Byte,這個結構共有16 bytes。

typedef struct udpbuf {

  char prefix;//前置字元,可以用來劃分資料的類別。

  unsigned char drvmode;//行駛模式,存上面enum那三種之一,1,2,3

  bool moveheadfw;//是否為前進,還是後退。簡單用一個布林資料儲存,省空間。

  float movewheeldiff;//兩顆馬達的差速比例,校正PWM對馬達轉速

  unsigned char movebotspeed;//自走車的行進速度,馬達的 PWM,0 - 254

  unsigned long start_milli;//這個步驟的起始時間 ms,用 millis()函數存入

} udpbuf_t;

//程式設計,模擬四個自走車運動模式,從0到3,轉彎,原地旋轉、前進、後退,一直切換、循環,記在 loopstep這個變數中,並由botdrive_loop 這個函數來執行要傳送所有變數的數值。

int loopstep = 3;//先預設最後一個 botdrive_loop,第一次執行 loopstep = (loopstep + 1) % 4 這行以後,會進入第0個運動,之後就以 0, 1, 2, 3, 0, 1, 2, 3, 0, 1.....的順序重複執行下去。

unsigned long moveinterval;//儲存目前運動延續時間,超過時間後,才執行下一個運動

unsigned long start_milli;//儲存運動開始時間


//Wifi UDP 相關參數

char ssid[] = "ASUS_SSID";//WiFi AP 名稱,按照你自己的狀況改

char pass[] = "123456789";//WiFi AP WPA密碼,按照你自己的狀況改

int status = WL_IDLE_STATUS;//WiFi目前狀態

unsigned int localPort = 10002;//ESP-01這端的 UDP Port,按照你自己的狀況改

unsigned int udpsvrport = 10002;  //遠端UDP伺服器的UDP Port,按照你自己的狀況改

IPAddress udpsvrip(192, 168, 11, 50);//UDP伺服器的IP,按照你自己的狀況改

int udppacketsize;//封包的byte大小,等等會用sizeof計算struct udpbuf的大小

unsigned char recvbuf[255];//接收封包用的緩衝區,設成 255 bytes,看你遠端的 UDP Server,會傳送多長的回應而定。

udpbuf_t *p_databuf;//傳送封包資料的緩衝區指標


WiFiEspUDP Udp;//建立UDP通訊物件,這個物件,呼叫 WiFiEspUDP 函式庫的類別。


void setup() {

  p_databuf = new udpbuf_t;//初始化指標,開一塊udpbuf_t結構大小的記憶體,給p_databuf指標

  udppacketsize = sizeof(struct udpbuf);//計算等等傳送這個結構的封包大小 (16 bytes)

  //設定兩個 Serial Port,實體 Serial 給 Serial Monitor看運作過程、狀況。ESPSerial 這個 Software Serial,則拿來接 ESP-01

  Serial.begin(115200);//Baud rate 根據實際Arduino IDE,Serial Monitor的Baud Rate設定改動

  ESPSerial.begin(57600);//要遷就 Software Serial的極速能力,以及ESP-01的鮑率設定。用Software Serial,建議把ESP-01的鮑率改成57600,比較不會遇到問題。

  

  //啟動ESP01連線,確認 Arduino 跟 ESP-01間,連線無誤。

  WiFi.init(&ESPSerial);

  // 檢查 ESP-01是否連線通訊

  if (WiFi.status() == WL_NO_SHIELD) {

    Serial.println("WiFi shield not present");

    // 如果ESP-01不存在,或沒有通訊上,則不要繼續執行。卡在 while (true)

    while (true);

  }

  // Arduino跟ESP-01如果能順利通訊上,則試著連線無線分享器,最後印出無線分享器配置內容,寫在程式後面的printWifiStatus()函數中。

  while ( status != WL_CONNECTED) {

    Serial.print("Attempting to connect to WPA SSID: ");

    Serial.println(ssid);

    // 連接到 WPA/WPA2 網路

    status = WiFi.begin(ssid, pass);

  }

  Serial.println("Connected to wifi");

  printWifiStatus();


  Serial.println("\nStarting connection to server...");

  

  // 設定UDP通訊埠,如同上面提到的,ESP-01這端的通訊埠,設定成 10002

  Udp.begin(localPort);

  Serial.print("Listening on port ");

  Serial.println(localPort);


  start_milli = millis();//啟動計時器,等等每個步驟,如果超過 moveinterval 時間,就會跳到下一個運動步驟。

}


void loop() {

  // 如果已經超過預定步驟時間,執行下一個botdrive_loop。第一次執行,會從步驟3跳到步驟0

  if (millis() - start_milli > moveinterval)//相減不用擔心在計數器計算一輪以後溢位。原理網路上可以搜尋到

  {

    loopstep = (loopstep + 1) % 4;//進入下一種移動,並循環。% 是除法餘數運算,累加到4的時候,會變成0

    botdrive_loop(loopstep);//設定運動參數,在這個程式中,每個步驟模擬自走車目前的方向、馬達差動、速度、運動模式等數值,然後放進去udpbuf結構裡面

    //發送目前的botdrive_loop參數給UDP Server,將udpbuf結構,利用C++內建的轉換函數 reinterpret_cast,把整個 udpbuf 結構,轉換成16個字元組,傳送出去。傳送內容的函式 Udp.write,必須被 Udp.beginPacket()跟Udp.endPacket()前後夾著。這是因為 ESP8266 的 AT指令,就是這麼定義的。你可以參考上面的 AT 指令操作過程就理解。

    Serial.print("Sent to UDP Server: ");

    Udp.beginPacket(udpsvrip, udpsvrport);

    Udp.write(reinterpret_cast<unsigned char*> (p_databuf), sizeof(struct udpbuf));

    Udp.endPacket();

    start_milli = millis();//重計開始時間

  }

  

  // 接收來自UDP Server的 packet。如果 packetSize不是0,代表 UDP Server這邊有回應或指令過來

  int packetSize = Udp.parsePacket();

  if (packetSize) {

    //以下印出一些 UDP Server的資料到 Serial Monitor

    Serial.print("Received packet of size ");

    Serial.println(packetSize);

    Serial.print("From ");

    IPAddress remoteIp = Udp.remoteIP();

    Serial.print(remoteIp);

    Serial.print(", port ");

    Serial.println(Udp.remotePort());


    // 實際讀取來自UDP Server的封包,並列印出來。這裡沒有解譯UDP Server 的資料,直接印出 Raw Data

    int len = Udp.read(recvbuf, udppacketsize);

    if (len > 0) {

      recvbuf[len] = 0;

    }

    Serial.println("Response:");

    Serial.println((char *)recvbuf);

  }

}

//下面這個函數就是模擬自走車運動狀況,並準備相關的數據,等等傳送出去。moveinterval 2000,是讓 loop()過兩秒之後,再執行下一個運動。這裡不用 Delay(2000),程式才不會卡在那裡浪費兩秒。prefix這個,是一個指令字元的設置,可以用這個字元,讓遠端伺服器解譯的時候,可以區分不同的資料。實際應用,不用這樣循環,就是直接把每步動作的資料,包進去一個結構,整包傳出去就好。

//舉例來說,假設你有 p_databuf 跟 p_sensordatabuf,兩個struct 資料,而且內含的參數種類不一樣。你可以把第一個字元,拿來區分這兩種資料。那麼你在 UDP Server 那端,寫下的接收程式,可以先抓出第一個字元,也就是 prefix,然後根據不同字元,個別用不同的解碼函數,解出正確的資料。

void botdrive_loop (unsigned char loopstep) {

  switch(loopstep){

    case 0:

      moveinterval = 2000;

      p_databuf->prefix = 'A';

      p_databuf->moveheadfw = true;

      p_databuf->movewheeldiff = 1.0;

      p_databuf->movebotspeed = 180;

      p_databuf->drvmode = MOVE_TURN;

      break;

    case 1:

      moveinterval = 4000;

      p_databuf->prefix = 'B';

      p_databuf->moveheadfw = true;

      p_databuf->movewheeldiff = -1.0;

      p_databuf->movebotspeed = 90;

      p_databuf->drvmode = MOVE_ROTATE;

      break;

    case 2:

      p_databuf->prefix = 'C';

      p_databuf->moveheadfw = true;

      p_databuf->movewheeldiff = 0.0;

      p_databuf->movebotspeed = 180;

      p_databuf->drvmode = MOVE_STRAIGHT;

      break;

    case 3:

      moveinterval = 8000;

      p_databuf->prefix = 'D';

      p_databuf->moveheadfw = false;

      p_databuf->movewheeldiff = 0.0;

      p_databuf->movebotspeed = 90;

      p_databuf->drvmode = MOVE_STRAIGHT;

      break;

  }

}


//這個函數用來列印 ESP-01 連線到WiFi AP後的相關資料,讓你在 Serial Monitor中,可以看到運作的狀況。在 setup()裡面用一次。

void printWifiStatus() {

  // print the SSID of the network you're attached to:

  Serial.print("SSID: ");

  Serial.println(WiFi.SSID());


  // print your WiFi shield's IP address:

  IPAddress ip = WiFi.localIP();

  Serial.print("IP Address: ");

  Serial.println(ip);


  // print the received signal strength:

  long rssi = WiFi.RSSI();

  Serial.print("signal strength (RSSI):");

  Serial.print(rssi);

  Serial.println(" dBm");

}

UDP Server 伺服器範例程式 (Python)

UDP Server 用 Python 來寫,這樣無論你是在 Windows 或者在 Linux,都可以很簡單在文字模式下,執行、觀看運作的結果,而不用寫比較複雜的 Windows 程式。如果你還沒學 Python,應該也很容易看得懂。把下面內容,以文字檔存入 udpserver.py 這個檔,然後執行 python udpservey.py 就行了。以下是 Python 3,如果你還在 Python 2 會有錯出現。

udpserver.py 範例程式

import socket
import struct

localIP     = "192.168.11.50"#伺服器本地的 IP
localPort   = 10002#伺服器這邊的UDP port
bufferSize  = 20#上面的範例,用到16bytes,故意寫大一點
 

# 建立 UDP 伺服器的 Socket,參數 socket.SOCK_DGRAM代表 UDP 通訊協定
# 詳細設定參考這裡 https://docs.python.org/3/library/socket.html
UDPServerSocket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)

# 將 IP 跟連接埠,綁入 Socket 中

UDPServerSocket.bind((localIP, localPort))
 
# 列印UDP Server開始運作的訊息
print("UDP server up and listening")

# 開始聽取 Client 送過來的資料
while(True):
    bytesAddressPair = UDPServerSocket.recvmsg(bufferSize)#接收到的資料,會放在Array中
    databuf = bytesAddressPair[0]#主要內容在array[0]
    print("Raw Data:", databuf)
    print("Data Len:", len(bytesAddressPair[0]))#了解一筆資料有多長
    address = bytesAddressPair[3]#Client 的IP, Port,放在 Array[3]裡面
    print("Address: ", address)
    datarecv = struct.unpack('cB?fBi', bytesAddressPair[0][:16])#將Raw Data解碼,'cB?fBi' 這個奇怪的字串,告訴 unpack 傳來字串的資料結構 c:字元 B:unsigned char ?:布林 f:float i:Integer
    然後解開的位元組串,還原成每種資料格式,內容存在 truple中,你就可以個別拿去運算。
    print("Data: ", datarecv)
       
    # 然後,用明碼回覆 Client 端,說xx資料已經收到了。在 Arduino程式中,就會接到回應
    bytesToSend = str.encode("UDP Received:{}".format(datarecv[0].decode()))

最後這行,你可以按照上面 Arduino 那邊的寫法,一整組命令資料,用 struct.pack 包成封包資料,然後整組送過去 ESP-01+Arduino (或STM32那邊)然後在那邊,寫一個解碼的程式,把命令資料解出來,然後按照 Server 這邊的指示執行(比方說,改變自走車的行進方向、速度等等)。

後記

以上的過程,可以讓你實現 Arduino 跟 伺服器端的快速遠端資料交換,在遠處根據機器回報的資料,控制機器運作,或蒐集即時資料,進行 Deep Learning 的 AI學習,發送控制指令給 Arduino端,執行智慧化的工作。

沒有留言:

張貼留言