這樣才完成開無線外掛的目的。不過,總不能每次都這樣子敲一大堆 AT 指令吧?本文接續前文,繼續介紹完整的外掛控制方法,包含讓你在遠端執行一個 UDP 伺服器,實際收取 Arduino Nano 這端的資料。
然後,你可以蒐集感測器的資料、控制、或執行大量的運算。比如,在伺服器這頭,蒐集來自Arduino 這頭的大數據,並執行 Deep Learning 訓練,同時也可以即時下命令給 Arduino 這端。
連接 ESP-01,相關硬體設定與注意事項
前文《接一片ESP-01》已經介紹過硬體的連接方式,這邊就不再贅述。由於通訊過程中,想要知道通訊的狀況,因此本文還是以上次的方法,開兩個 Serial,其中一個用 Software Serial 代替。需要注意的有以下幾點:
- ESP-01 的電源跟訊號是 3.3V ,要小心
- ESP-01 有時瞬間電流可能到 600mA,放一個電容比較穩定
- 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的降壓模組,電力夠也不會發燙。像以下照片這種降壓模組,背面有幾個電壓焊點選擇,用銲錫接起來,就可以直接輸出固定的電壓,很方便。
用AT指令傳送資料給 UDP 伺服器
- 將 ESP-01 設成 SoftAP + Station Mode
- 連接 Wifi 無線分享器,輸入分享器的 SSID 跟密碼
- 驗證配發的 IP
- 設定多方連接 (AT+CIPMUX=1)
- 啟動本地 UDP 通信協定 (192.168.11.169),用 Link ID 3 (因為之前設定多方連接,可以從0-4隨便選一個,如果真的要多方接,那就一個遠方IP固定用一個號碼,最多連接五個地方)
- 連線遠方UDP伺服器 (192.168.11.50),準備傳送資料。將本地方跟遠方的 Port,都設定成 10002 (兩邊的Port可以不一樣),最後一個2,代表建立後,遠端的UDP對象可以變。等等下 AT+CIPSEND 加上對方的 IP,可以改,一對多UDP Server的狀況下可以這樣用。
- 用3號LinkID,開始傳送 7個 byte 位元組的資料
- 輸入7個位元組'abcdefg',如果你先按前四個字元(每個字元1個byte),ESP-01會繼續等你敲入剩下3個字元。
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");
}
沒有留言:
張貼留言