三、使用範例與說明:
在本章節中將會透過幾個範例為讀者示範如何去實現及實作前面一節中不同拓樸組態的配置,此外在我們的範例程式中,不需要特別去區分所是用的模組是ESP8266或是ESP32,單一程式可以無落差的直接套用在兩種模組上,這也是我們這個範例程式最大的特點之一!除此之外,為了避免在傳輸發送前的配對還必須先得知接收模組的MAC位址,在我們的程式中都會採用廣播式的方式發送資料,因此便可省略了必須先知悉接收裝置MAC位址碼的問題!
三、1單向一對一/點對點(peer to peer)架構
在這個小節中將示範『二、1 一對一單向傳輸』這個ESP-NOW最基本拓樸結構的通信方式,而在介紹範例程式之前,先讓我們來看一下在ESP-NOW常會使用到的一些指令函數,以下便是 ESP-NOW 最基本功能的幾個指令函數摘要:
函數名稱和說明
⬛ esp_now_init(),初始化 ESP-NOW。但是你必須在初始化 ESP-NOW 之前先初始化 Wi-Fi功能,如果成功,則返回 0。
⬛ esp_now_set_self_role(role),自我腳色設定函式,role這個參數可以是:ESP_NOW_ROLE_IDLE=0、ESP_NOW_ROLE_CONTROLLER、ESP_NOW_ROLE_SLAVE、ESP_NOW_ROLE_COMBO、ESP_NOW_ROLE_MAX
⬛ esp_now_add_peer(uint8 mac_addr, uint8 role, uint8 channel, uint8 key, uint8 key_len),呼叫此函數可用來配對設備。
⬛ esp_now_send(uint8 mac_address,uint8 data,int len),資料數據發送函式,可用來發送data這部分的數據。
⬛ esp_now_register_send_cb(onDataSend),註冊一個在發送數據時觸發的回應函數(即onDataSend),發送訊息時會呼叫這個函數,而此函數可用以返回發送是否成功的訊息。
⬛ esp_now_register_rcv_cb(onDataRecv),註冊一個接收數據時觸發的回應函數(即onDataRecv)。當通過 ESP-NOW 接收到數據時,會呼叫這一個函數。
◎ 發送端程式列表與說明:
本次範例所使用的開發工具還是採用最常見的Arduino IDE,在撰寫ESP-NOW程式時有兩個問題最讓人頭痛,一是要先查出接收端的MAC位址,二是在使用ESP8266或是ESP32模組時,不管程式或引用的函式庫都不一樣;雖然現在大家都漸漸改用ESP32了,可是如果我們只是單純要控制一個開關,這時假如使用ESP32加上一個繼電器或是模組,在成本上有些說不過去;這時如果採用一個由ESP-01(核心晶片為ES8266)構成繼電器模組,就會是一個不錯的選擇。可是使用過Arduino IDE的朋友就會有個感覺,就是晶片種類切換來切換去還真有些麻煩,更不要說因為模組的不同所需改換的函式庫及指令函數等問題更是煩人;為了讓程式更容易使用,筆者花了不少時間去調整程式,終於一次把前述兩個問題給解決了!
以下是發送端的完整程式列表:
#ifdef ESP32
#include <WiFi.h>
#include <esp_now.h>
esp_now_peer_info_t peerInfo;
int ledOn=1;
int ledOff=0;
#else
#include <ESP8266WiFi.h>
#include <espnow.h>
int ledOn=0;
int ledOff=1;
#endif
uint8_t macAddr[6],broadCastMacAddr[6]={0xff,0xFf,0xff,0xff,0xff,0xff};
String ESPmac="";
typedef struct message{
int ID;
char a[32];
int b;
unsigned int c;
float d;
bool e;
} message;
message myData;
byte myId=1,LED=2;
unsigned int sendTimes=0;
unsigned long lastTime=0;
unsigned long timerDelay=5000;
void setup() {
Serial.begin(115200);
Serial.println();
WiFi.macAddress(macAddr);
for(int i=0;i<6;i++) {
if (macAddr[i] < 16)
ESPmac+="0";
ESPmac+=String(macAddr[i],HEX);
}
Serial.print("ESP mac Addrres = ");
Serial.println(ESPmac);
// WiFi.softAP mac Address(macAddr);
WiFi.mode(WIFI_STA);
if(esp_now_init() !=0) {
Serial.println("Error initializing ESP-NOW!");
while(1) {
digitalWrite(LED,0);
delay(100);
digitalWrite(LED,1);
delay(100);
}
}
#ifdef ESP32
esp_now_register_send_cb(onDataSent);
memcpy(peerInfo.peer_addr, broadCastMacAddr, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
//Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
#else
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);
esp_now_register_send_cb(OnDataSent);
#endif
lastTime=millis();
}
void loop() {
if((millis()-lastTime) > timerDelay)
{
lastTime=millis();
sendTimes++;
Serial.print("目前為第 ");Serial.print(sendTimes);Serial.println(" 筆資料,資料已發送完成!");
myData.ID=myId;
strcpy(myData.a,"This is a Char");
myData.b=random(1,100);
myData.c=sendTimes;
myData.d=1.23;
myData.e=true;
esp_now_send(broadCastMacAddr,(uint8_t *)&myData,sizeof(myData));
}
}
// ESP 使用的資料發送callback 副程式:
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)
{
Serial.print("最後一筆資料傳送狀況: ");
if (sendStatus == 0)
Serial.println("傳送成功!\n");
else
Serial.println("傳送失敗!\n");
}
// ESP32 使用的資料發送callback 副程式:
#ifdef ESP32
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("最後一筆資料傳送狀況: ");
if (status == 0)
Serial.println("傳送成功!\n");
else
Serial.println("傳送失敗!\n");
}
#endif
發送端程式列表
對於一個發送端的裝置而言必須執行下面的動作:
初始化 ESP-NOW(記得要先初始化WiFi功能)。
在發送數據時註冊一個回應函數 — 『OnDataSent』,以便在發送消息時執行此函數,而此函數可以告訴我們是否成功發送資料數據。
添加對應的設備(接收方),為此您需要知道接收方的 MAC 地址。
向對應設備發送消息。
為了讓程式能同時使用在ESP8266與ESP32兩種模組上,因此在程式一開始的引入(include)部份我們就必須先處理所使用的相關函式庫及相關變數,下面的程式碼便是我們這個發送範例程式的引入部分。
由於啟動ESP-NOW的初始化功能前必須先啟動ESPxx的WiFi功能,在此我們用”#ifdef … #esle … #endif”的編譯前置指令,讓我們的Arduino IDE自動依所選用的ESPxx模組不同,去引入不同的函式庫;對ESP32而言,必須用到”<WiFi.h>”和”<esp_now.h>”這兩個函式庫,而對ESP8266來說,則是”<ESP866WiFi.h>”及”<espnow.h>”。
在我們的程式中為了確定ESP-NOW的初始化功能是否成功﹖使用了ESP8266郵票板上內建的LED(接在GPIO 2上)做為指示燈,對ESP32而言,它的郵票板並沒有這顆LED,一般是作在擴充模組板(例如NodeMCU)上,而且亮滅的電壓位準和ESP8266相反,為了相容起見,我們用「ledOn」、「ledOff」這兩種個標記來代表LED的亮滅。
#ifdef ESP32
#include <WiFi.h>
#include <esp_now.h>
esp_now_peer_info_t peerInfo;
int ledOn=1;
int ledOff=0;
#else
#include <ESP8266WiFi.h>
#include <espnow.h>
int ledOn=0;
int ledOff=1;
#endif
為了避開必須先知道接收端的MAC位址才能發出資料的困擾,在此我們是使用廣播(Broadcast)的方式來傳送資料,也就是說發射端傳送的資料所有的接收板(假如還有其他的話)都會接收到,如果接收端要知道是誰發送,除了可以由發送者的MAC位址得知之外,也可以在發送的資料中包含一個發送板所賦予的ID編號去辨識。
下面所定義的變數中,這個六位元組內容都為0xff的「boardCastMacAddr」,便是ESP-NOW中用來標示為廣播用的MAC位址變數。
uint8_t macAddr[6],broadCastMacAddr[6]={0xff,0xFf,0xff,0xff,0xff,0xff};
為了方便展示所能傳送的資料種類,在此我們定義了一可包括各種資料型態的結構變數「message」;在其中包括了整數(int)、長整數(long)、浮點數(float)、布林數(bool)及字串(char [])等變數型態,其內容如下面程式所示。其中的「ID」用來傳送發送板所賦予的ID編號,而「c」則是代表目前發送的資料筆數流水編號。
至於後面的「myData」則是我們實際會傳送的型態為「message」的結構變數,而「MyId」是這塊送發送板所賦予的ID編號變數,在此為1,使用者可依自己的需要改成其他的號碼。
typedef struct message{
int ID;
char a[32];
int b;
unsigned long c;
float d;
bool e;
} message;
message myData;
byte myId=1,LED=2;
在前面常用指令函數介紹時說過,當要初始化ESP-NOW之前必須先初始化Wi-Fi功能,因此在下面屬於初始化(setup())部分的程式中,我們先用『WiFi.mode(WIFI_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化,而且設定為STA模式。然後呼叫『esp_now_init()』這個ESP-NOW初始化用的指令函數,如果傳回來的結果是錯誤的,則會在Arduino IDE的監控視窗中顯示"Error initializing ESP-NOW!"這樣的錯誤提示訊息,而且會令發送板進入一個讓接在GPIO 2的LED快速閃爍的無窮迴圈,因為如果初始化失敗,系統就不應該再動作下去!
WiFi.mode(WIFI_STA);
if(esp_now_init() !=0) {
Serial.println("Error initializing ESP-NOW!");
while(1) {
digitalWrite(LED,0);
delay(100);
digitalWrite(LED,1);
delay(100);
}
}
在初始化完ESP-NOW之後接下來的動作就是註冊一個回應函數,及添加對應的接收方設備的 MAC 地址,由於ESP32、ESP8266兩者對這些動作使用的指令及方法並不相同,所以在我們的範例程式中一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種晶片上。
下面這段程式是針對ESP32而來的,首先用『esp_now_register_send_cb』這個指令函數去註冊一個回應函數「onDataSent」,然後再用『esp_now_add_peer』這個指令函數指定接收端的MAC位址「peefInfo」,不過我們必須先將廣播用的MAC位址「broadCastMacAddr」複製到「peefInfo」上。
#ifdef ESP32
esp_now_register_send_cb(onDataSent);
memcpy(peerInfo.peer_addr, broadCastMacAddr, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
//Add peer
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer");
return;
}
對於ESP8266來說必須先定義自己的腳色,也就是發送端,在此使用『esp_now_set_self_role』這個指令函數去設定為發送者,其參數值為「ESP_NOW_ROLE_CONTROLLER」;至於指定接收端MAC位址的指令函數『esp_now_add_peer』,它引用參數的方式與內容跟ESP32是不一樣的,這點還請讀者多注意!至於去註冊一個回應函數『onDataSent』則是一樣使用『esp_now_register_send_cb』這個指令函數。
#else
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);
esp_now_register_send_cb(OnDataSent);
#endif
下面部分則是主迴圈(loop())部分的所有程式碼,內容很簡單,就是在固定的時間(timerDelay🡺5秒)發送一次資料(myData),每發送一次「sendTimes」這個變數會加一,用以代表資料的筆數;在設定好「myData」這個資料結構變數的內容之後,最後呼叫『esp_now_send』這個指令函數把資料發送出去,這樣便完成一次ESP-NOW的資料傳輸動作。
void loop() {
if((millis()-lastTime) > timerDelay)
{
lastTime=millis();
sendTimes++;
Serial.print("目前為第 ");Serial.print(sendTimes);Serial.println(" 筆資料,資料已發送完成!");
myData.ID=myId;
strcpy(myData.a,"This is a Char");
myData.b=random(1,100);
myData.c=sendTimes;
myData.d=1.23;
myData.e=true;
esp_now_send(broadCastMacAddr,(uint8_t *)&myData,sizeof(myData));
}
}
下面的程式是發送回應函數『onDataSent』的主體內容,同樣的因為ESP32、ESP8266兩者使用的程式指令及方法並不相同,所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種晶片上。
在這個資料發送callback副程式中,同樣的都會在Arduino IDE的監控視窗中先顯示"第 ?? 筆資料傳送狀況:"的提示訊息,其中的”??”就是「sendTimes」這個變數的內容,如果發送成功,會顯示"傳送成功!"的提示訊息,否則將顯示"傳送失敗!"的錯誤提示訊息。
// ESP 使用的資料發送callback 副程式:
#ifdef ESP32
// ESP32 使用的資料發送callback 副程式:
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("第 ");Serial.print(sendTimes);Serial.print(" 筆資料傳送狀況: ");
if (status == 0)
Serial.println("傳送成功!\n");
else
Serial.println("傳送失敗!\n");
}
#else
void OnDataSent(uint8_t *mac_addr, uint8_t sendStatus)
{
Serial.print("第 ");Serial.print(sendTimes);Serial.print(" 筆資料傳送狀況: ");
if (sendStatus == 0)
Serial.println("傳送成功!\n");
else
Serial.println("傳送失敗!\n");
}
#endif
◎ 執行結果:
為測試程式是否能同時相容於ESP32及ESP8266,發送端程式是燒錄到ESP32上;下圖為發送端裝置(即ESP32)傳送到Arduino IDE監控視窗中的提示訊息,在其中會顯示所傳送資料的筆數及是否傳送成功。
發送端顯示於Arduino IDE監控視窗中之訊息
◎ 接收端程式列表與說明:
下列程式為接收端的完整程式列表,其中前面1~28行的函式庫引入與變數定義部分,和前面的發送端程式相同,在此就不重複說明。本範例程式和一般在網路上所見最大的不同點,就是在程式中我們會令ESPxx裝置在WiFi功能中工作在AP+STA模式底下,且會啟動soft-AP功能,並設定一個SSID名稱,而這個AP的SSID名稱中將會包含了這顆ESPxx晶片的MAC位址訊息。
之所以要這麼作,是為了後面延伸的應用,如此一來當我們想要只針對特定ESPxx裝置傳送資料時,就可以不必先把固定的接收端MAC位址用燒進的方式放進發送端的程式裡,我們可以透過WiFi掃描的方式得知有那些接收裝置在我們可及的範圍內,只要把它們AP的SSID名稱中的MAC位址部分萃取出來配對就可以了。
#ifdef ESP32
#include <WiFi.h>
#include <esp_now.h>
int ledOn = 1;
int ledOff = 0;
#else
#include <ESP8266WiFi.h>
#include <espnow.h>
int ledOn = 0;
int ledOff = 1;
#endif
typedef struct message {
int ID;
char a[32];
int b;
unsigned long c;
float d;
bool e;
} message;
// Create a struct_message called myData
message myData;
//uint8_t macAddr[6];
uint8_t myMacAddr[6];
String myMacSoftSSID = "";
byte LED = 2;
void setup() {
Serial.begin(115200); Serial.println();
pinMode(LED, OUTPUT);
digitalWrite(LED, 1);
Serial.print("本機的 MAC位址為: "); Serial.println(WiFi.macAddress());
WiFi.macAddress(myMacAddr);
myMacSoftSSID = "ESPxx_";
for (int i = 0; i < 6; i++)
{
if (myMacAddr[i] < 16)
myMacSoftSSID += "0";
myMacSoftSSID += String(myMacAddr[i], HEX);
}
Serial.print("本機的 MAC位址AP是: ");
Serial.println(myMacSoftSSID);
WiFi.mode(WIFI_AP_STA);
char MyMacSoftSSID[30];
myMacSoftSSID.toCharArray(MyMacSoftSSID, myMacSoftSSID.length());
WiFi.softAP(MyMacSoftSSID);
// Init ESP-NOW
if (esp_now_init() != 0) {
Serial.println("ESP-NOW初始化錯誤!");
return;
}
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
delay(1);
}
// callback function that will be executed when data is received
#ifdef ESP32
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *inComingData, int len) {
#else
void OnDataRecv(uint8_t *mac_addr, uint8_t *inComingData, uint8_t len) {
#endif
char macStr[18];
Serial.print("本筆資料接收自下列MAC位址: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);
memcpy(&myData, inComingData, sizeof(myData));
Serial.print("Bytes received: "); Serial.println(len);
Serial.print("Received Times: "); Serial.println(myData.bb);
Serial.print("Char: "); Serial.println(myData.a);
Serial.print("Int: "); Serial.println(myData.b);
Serial.print("Float: "); Serial.println(myData.c);
Serial.print("Bool: "); Serial.println(myData.d);
Serial.println();
}
接收端程式列表
在前面說過,當要初始化ESP-NOW之前必須先初始化Wi-Fi功能,在下面屬於初始化(setup())部分的程式中,我們用『WiFi.mode(WIFI_AP_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化,而且設定為AP+STA模式。所以要這樣設定,是因為想讓外界能透過本裝置所顯示的soft-AP的SSID稱就能得知本裝置的MAC位址,也就是說如果我們用其他WiFi裝置來掃描這塊ESPxx模組時,會看到”ESP_xxxxxxxxxxxx”這樣的AP名稱,其中長度共12字元的’x’就是這個模組的6個bytes的MAC位址。
下面的程式在把本機MAC位址轉成字串變數「myMacSoftSSID」之後,也會順便在Arduino IDE監控視窗中顯示“本機的 MAC位址是: ESPxx_xxxxxxxxxxxx”的提示訊息,以供使用者參考。
WiFi.macAddress(myMacAddr);
myMacSoftSSID = "ESPxx_";
for (int i = 0; i < 6; i++)
{
if (myMacAddr[i] < 16)
myMacSoftSSID += "0";
myMacSoftSSID += String(myMacAddr[i], HEX);
}
Serial.print("本機的 MAC位址是: ");
Serial.println(myMacSoftSSID);
WiFi.mode(WIFI_AP_STA);
char MyMacSoftSSID[30];
myMacSoftSSID.toCharArray(MyMacSoftSSID, myMacSoftSSID.length());
WiFi.softAP(MyMacSoftSSID);
接著程式使用『esp_now_init()』這個ESP-NOW初始化用的指令函數,如果傳回來的結果是錯誤的(不等於0),則會在Arduino IDE的監控視窗中顯示 ”ESP-NOW初始化錯誤!” 這樣的錯誤提示訊息。
// Init ESP-NOW
if (esp_now_init() != 0) {
Serial.println("ESP-NOW初始化錯誤!");
return;
}
下面是用『esp_now_register_recv_cb』這個函數指令去登錄『OnDataRecv』為接收服務副程式,這個『OnDataRecv』接收服務副程式會在裝置接收到資料時被啟動,使用者可以利用這個副程式執行想要做的動作,例如打開或關閉一個開關。
esp_now_register_recv_cb(OnDataRecv);
一般來說我們的主迴圈程式區可以是空的,但為了避免閒置時間過久以致看門狗機制啟動,所以在其中加上一個delay,以便定時清除看門狗計時器。
void loop() {
delay(1);
}
下面的程式是接收服務副程式『onDataRecv』的主體內容,同樣的因為ESP32、ESP8266兩者使用的程式指令及方法並不相同,所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種不同的晶片上。其實種兩種晶片的副程式差距很小,主要是在所使用的引數宣告的方式略有不同而已,至於程式主體的部分則是完全一樣。
當這個接收服務副程式因為裝置接收到數據被觸發之後,會從「inComingData」這個結構變數中把對應的數據萃取出來,然後顯示在Arduino IDE監控視窗中。
// callback function that will be executed when data is received
#ifdef ESP32
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *inComingData, int len) {
#else
void OnDataRecv(uint8_t *mac_addr, uint8_t *inComingData, uint8_t len) {
#endif
char macStr[18];
Serial.print("本筆資料接收自下列MAC位址: ");
snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);
memcpy(&myData, inComingData, sizeof(myData));
Serial.print("Bytes received: "); Serial.println(len);
Serial.print("Received Times: "); Serial.println(myData.bb);
Serial.print("Char: "); Serial.println(myData.a);
Serial.print("Int: "); Serial.println(myData.b);
Serial.print("Float: "); Serial.println(myData.c);
Serial.print("Bool: "); Serial.println(myData.d);
Serial.println();
}
◎ 執行結果:
下面的圖案是接收端於Arduino IDE監控視窗中顯示從發送端所接收到的訊息畫面,其中標記1是發送裝置所賦予的ID號碼,而標記2而是這筆資料的流水編號,每次會加1;至於後面的內容可依使用者的需求自行安排設定,例如設定某一支I/O腳的輸出狀態,以用來控制某個開關或繼電器的動作。
接收端於Arduino IDE監控視窗中顯示之訊息
沒有留言:
張貼留言