在這個小節中將示範『二、6雙向多對多傳輸』這個ESP-NOW中最複雜的拓樸結構的通信方式,照理來說應該循序漸進從簡至繁陸續介紹其他各種ESP-NOW拓樸結構的通信方式,不過在設計『二、2 一對一雙向傳輸』這種拓樸架構的程式時發現,由於我們的發送端是以廣播的方式發送資料,也就是說只要是在電波信號可及的場域中,所有的ESPxx裝置都可以收到這些數據,這麼一來就等同於建構了『二、6雙向多對多傳輸』這個拓樸結構的通信方式,既然這樣就乾脆一次到位,直接為大家介紹這種拓樸結構就好了
雙向多對多傳輸
◎ 發送端程式列表與說明:
在這個範例中會示範多片的ESPxx(可混合使用ESP8266與ESP32兩種晶片)模組板互相通信傳輸資料的方法,其主要特點如下
本範例程式可同時給ESP8266與ESP32兩種不同晶片的模組板使用,不必做任何的修改。
本範例程式可同時使用在發送與接收端的模組板,也就是說一個程式即可涵蓋所有的拓樸模式與主從角色,當然也不用再做任何的修改。
在這個範例中,我們將有兩塊 ESP8266 和一塊ESP32模組板作為整個系統的成員,每塊板ESPxx模組板都具有雙向通信的功能,而且它們發送資料的方式是以廣播的方為之,也就是其它的ESPxx模組板都會接收到任一發送裝置發送出去的資料。
每塊板ESPxx模組板都具有WiFi AP的腳色,它們的SSID名稱為其晶片的種類加上他的MAC位址,例如ESP8266晶片它的AP SSID名稱為「ESP8266_xxxxxxxxxxxx」,其中的12個’x’便是6個bytes的MAC位址值。當使用者用手機去掃描這些ESPxx模組的WiFi AP點時,便可由它們的SSID名稱得知其MAC位址。
在系統中所發送的範例資料和上一節一樣為一結構變數,其中包括了不同種類的變數型態,而其中的字串變數將改成該模組的SSID名稱,以作為辨識發送者的ID名稱之用。
發送的範例資料結構變數中的布林變數,可用來控制ESPxx模組板上接在GPIO 2上的LED亮滅之用,若值為true則會點亮LED,反之如果是false則會令LED熄滅。
下列程式即為此雙向多對多收發程式的列表:
#ifdef ESP32
#include <WiFi.h>
#include <esp_now.h>
esp_now_peer_info_t peerInfo;
int LedOn=1;
int LedOff=0;
String ESPtype="ESP32_";
#else
#include <ESP8266WiFi.h>
#include <espnow.h>
int LedOn=0;
int LedOff=1;
String ESPtype="ESP8266_";
#endif
uint8_t macAddr[6],broadCastMacAddr[6]={0xff,0xFf,0xff,0xff,0xff,0xff};
String ESPmac="";
typedef struct message{
char a[32];
int b;
unsigned long bb;
float c;
bool d;
} message;
message myData;
String myMacSoftSSID="";
byte LED=2;
bool LedStatus=false;
unsigned int sendTimes=0;
unsigned long lastTime=0;
unsigned long timerDelay=10000;
void setup() {
Serial.begin(115200);
Serial.println();
pinMode(LED,OUTPUT);
WiFi.macAddress(macAddr);
for(int i=0;i<6;i++) {
if (macAddr[i] < 16)
ESPmac+="0";
ESPmac+=String(macAddr[i],HEX);
}
myMacSoftSSID=ESPtype+ESPmac;
Serial.print("ESP mac Addrres = ");
Serial.println(ESPmac);
// WiFi.softAP mac Address(macAddr);
char MyMacSoftSSID[30];
WiFi.mode(WIFI_AP_STA);
myMacSoftSSID.toCharArray(MyMacSoftSSID,myMacSoftSSID.length()+1);
WiFi.softAP(MyMacSoftSSID);
// Serial.print("WiFi 的 channel 號碼為: "); Serial.println(getWiFiChannel(ssid));
if(esp_now_init() !=0) {
Serial.println("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_COMBO);
esp_now_register_send_cb(OnDataSent);
esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);
#endif
esp_now_register_recv_cb(OnDataRecv);
lastTime=millis();
}
void loop() {
if((millis()-lastTime) > timerDelay)
{
lastTime=millis();
Serial.println("資料已發送完成!");
// strcpy(myData.a,"This is a Char");
myMacSoftSSID.toCharArray(myData.a,myMacSoftSSID.length()+1);
sendTimes++;
myData.bb=sendTimes;
myData.b=random(1,100);
myData.c=1.23;
LedStatus=!(LedStatus);
myData.d=LedStatus;
// Serial.println(myData.d);
esp_now_send(broadCastMacAddr,(uint8_t *)&myData,sizeof(myData));
}
}
// 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
// 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];
memcpy(&myData, inComingData, sizeof(myData));
Serial.print("接收到的位元組(bytes): "); Serial.println(len);
Serial.print("接收資料來源: "); Serial.println(myData.a);
Serial.print("接收的筆數: "); Serial.println(myData.bb);
Serial.print("整數: "); Serial.println(myData.b);
Serial.print("浮點數: "); Serial.println(myData.c);
Serial.print("布林數: "); Serial.println(myData.d);
if (myData.d == true)
{
Serial.println("LED點亮!");
digitalWrite(LED,LedOn);
}
else {
Serial.println("LED熄滅!");
digitalWrite(LED,LedOff);
}
Serial.println();
}
int32_t getWiFiChannel(const char *ssid) {
if (int32_t n = WiFi.scanNetworks()) {
for (uint8_t i=0; i<n; i++) {
if (!strcmp(ssid, WiFi.SSID(i).c_str())) {
return WiFi.channel(i);
}
}
}
return 0;
}
雙向多對多收發程式列表
下列程式為雙向多對多收發程式前面的函式庫引入與變數定義部分,為了讓程式能同時使用在ESP8266與ESP32兩種模組上,因此在程式一開始的引入(include)部份我們就必須先處理所使用的相關函式庫及相關變數,基本上和前一節的範例大致相同。
前面說過本範例程式和一般在網路上所見最大的不同點,就是在程式中我們會令ESPxx裝置在WiFi功能中工作在AP+STA模式底下,且會啟動soft-AP功能,並設定一個SSID名稱,而這個AP的SSID名稱中將會包含了這顆ESPxx晶片的MAC位址訊息,並且是以該晶片種類的編號開頭,這就是「ESPtype」這個新字串變數的用途。
#ifdef ESP32
#include <WiFi.h>
#include <esp_now.h>
esp_now_peer_info_t peerInfo;
int LedOn=1;
int LedOff=0;
String ESPtype="ESP32_";
#else
#include <ESP8266WiFi.h>
#include <espnow.h>
int LedOn=0;
int LedOff=1;
String ESPtype="ESP8266_";
#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 [])等變數型態,其內容如下面程式所示。至於後面的「myData」則是我們實際會傳送的型態為「message」的結構變數。在前一節的範例中我們用了一個整數作為送發送板的ID編號變數,不過這個ID編號還是必須在燒錄程式時給定,但這樣一來程式就太沒有彈性了!在此為了能動態的自行配置這個ID,我們的程式將會自動把前面提過的包含了這顆ESPxx晶片中類和MAC位址訊息的AP SSID名稱字串變數指定給字串變數「char []」,作為發送端的ID,這樣接收端從ID編號的訊息便可以得知發送者是哪一塊模組板了。
typedef struct message{
char a[32];
int b;
unsigned long bb;
float c;
bool d;
} message;
message myData;
String myMacSoftSSID="";
byte LED=2;
bool LedStatus=false;
unsigned int sendTimes=0;
unsigned long lastTime=0;
unsigned long timerDelay=10000;
至於下面的程式會先取得這顆ESPxx晶片的MAC位址訊息,並且與包含該晶片種類編號開頭的變數「ESPtype」組合後,得到「myMacSoftSSID」這個字串變數以作為模組板的AP SSID名稱。
WiFi.macAddress(macAddr);
for(int i=0;i<6;i++) {
if (macAddr[i] < 16)
ESPmac+="0";
ESPmac+=String(macAddr[i],HEX);
}
myMacSoftSSID=ESPtype+ESPmac;
Serial.print("ESP mac Addrres = ");
Serial.println(ESPmac);
在前面常說過,要初始化ESP-NOW之前必須先初始化Wi-Fi功能,因此在下面的程式中,我們先用『WiFi.mode(WIFI_AP_STA)』這行指令將ESPxx晶片進行Wi-Fi功能的初始化,而且設定為AP+STA模式。接著使用『WiFi.softAP(MyMacSoftSSID)』這個指令函數建立一個AP存取點,其名稱為「myMacSoftSSID」,以便外界可以得知這塊模組板的MAC位址值,好作為對特定MAC位址裝置傳送數據之用。
// WiFi.softAP mac Address(macAddr);
char MyMacSoftSSID[30];
WiFi.mode(WIFI_AP_STA);
myMacSoftSSID.toCharArray(MyMacSoftSSID,myMacSoftSSID.length()+1);
WiFi.softAP(MyMacSoftSSID);
然後呼叫『esp_now_init()』這個ESP-NOW初始化用的指令函數,如果傳回來的結果是錯誤的,則會在Arduino IDE的監控視窗中顯示" ESP-NOW!初始化失敗!"這樣的錯誤提示訊息,而且會令ESPxx模組板上接在GPIO 2的LED進入一個快速閃爍的無窮迴圈,以提醒使用者ESP-NOW初始化錯誤!
if(esp_now_init() !=0) {
Serial.println("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」共6個bytes複製到「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』這個指令函數。
對於ESP32和ESP8266來說,對於接收部分的設定是相同的,因此共用了下面『esp_now_register_recv_cb』這個函數指令去登錄『OnDataRecv』為接收服務副程式。
#else
esp_now_set_self_role(ESP_NOW_ROLE_COMBO);
esp_now_register_send_cb(OnDataSent);
esp_now_add_peer(broadCastMacAddr, ESP_NOW_ROLE_SLAVE,1, NULL, 0);
#endif
esp_now_register_recv_cb(OnDataRecv);
lastTime=millis();
}
下面部分則是主迴圈(loop())部分的所有程式碼,內容很簡單,就是在固定的時間(在此改為timerDelay🡺10秒)發送一次資料(myData),每發送一次「sendTimes」這個變數會加一,用以代表發送資料的筆數;在設定好「myData」這個資料結構變數的內容之後,最後呼叫『esp_now_send』這個指令函數把資料發送出去,這樣便完成一次ESP-NOW的資料傳輸動作。
「myData」這個資料結構變數的內容和上一節最大的不同之處,就是字串變數「char []」的內容改設定為包含了這顆ESPxx晶片種類及MAC位址訊息的AP SSID名稱變數「myMacSoftSSID」,作為發送端的ID;在此使用『myMacSoftSSID.toCharArray』這個延伸字串指令,把字串變數「myMacSoftSSID」轉換成「myData.a」這個字元陣列變數,以免變數型態不合編譯不過!
除此之外我們還把其中的布林數,也就是「myData.d」在接收端用來控制接在GPIO 2的LED亮滅,當接收值為”true”會點亮LED,反之則令LED熄滅。為了能讓LED能產生亮滅的效果,我們必須不斷改變這個布林數,在此每當發送事件啟動也就是10秒到了,會執行『LedStatus=!(LedStatus)』這行指令,讓該布林數值反相,以達到不斷交互改變的結果。
void loop() {
if((millis()-lastTime) > timerDelay)
{
lastTime=millis();
Serial.println("資料已發送完成!");
// strcpy(myData.a,"This is a Char");
myMacSoftSSID.toCharArray(myData.a,myMacSoftSSID.length()+1);
sendTimes++;
myData.bb=sendTimes;
myData.b=random(1,100);
myData.c=1.23;
LedStatus=!(LedStatus);
myData.d=LedStatus;
// Serial.println(myData.d);
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
再下來的程式是接收服務副程式『onDataRecv』的主體內容,同樣的因為ESP32、ESP8266兩者使用的程式指令及方法並不相同,所以在此我們一樣的採用”#ifdef … #esle … #endif”的編譯前置指令去把它區分出來,以便可以相容使用在兩種不同的晶片上。其實種兩種晶片的副程式差距很小,主要是在所使用的引數宣告的方式略有不同而已,至於程式主體的部分則是完全一樣。
當這個接收服務副程式因為裝置接收到數據被觸發之後,會從「inComingData」這個結構變數中把對應的數據萃取出來,然後顯示在Arduino IDE監控視窗中,並且把提示訊息都改用中文。這個結構變數的內容和上一節的範例大同小異,主要有兩個數據的功能不一樣,一是字元陣列「myData.a」也就是字串變數部分,顯示的是發送端晶片的種類及其MAC位址碼,再來就是布林數「myData.d」會被用來決定接在GPIO 2上的LED是點亮還是熄滅,並且會將結果顯示在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];
memcpy(&myData, inComingData, sizeof(myData));
Serial.print("接收到的位元組(bytes): "); Serial.println(len);
Serial.print("接收資料來源: "); Serial.println(myData.a);
Serial.print("接收的筆數: "); Serial.println(myData.bb);
Serial.print("整數: "); Serial.println(myData.b);
Serial.print("浮點數: "); Serial.println(myData.c);
Serial.print("布林數: "); Serial.println(myData.d);
if (myData.d == true)
{
Serial.println("LED點亮!");
digitalWrite(LED,LedOn);
}
else {
Serial.println("LED熄滅!");
digitalWrite(LED,LedOff);
}
Serial.println();
}
◎ 執行結果:
當我們把本範例系統配置的三片EXPxx模組板(兩片ESP8266加一片ESP32)接電啟動之後,開啟手機Wi-Fi掃描功能,便可看到類似下面圖片畫面的訊息;其中標記1、2是代表兩片ESP8266模組板的MAC位址數值的AP存取點SSID名稱,而標記3則是ESP32模組板的訊息。
至於在Arduino IDE監控視窗上,對不同的ESPxx模組板來說會看到不同接收訊息,以下圖來說就是本次範例系統中ESP32所看到內容,除了他本身發送資料的訊息(標記5)之外共有兩筆資料,分別是來自MAC位址為「a0:20:a6:14:41:22」(標記1)與「2c:f4:32:17:83:50」(標記2)這兩塊ESP8266模組板的接收資料;而且當接收到的布林數為’0’時會出現”LED熄滅”的訊息 (標記3) ,若布林數為’1’時的訊息改為”LED點亮” (標記4)。
下面是本範例系統中MAC位址為「a0:20:a6:14:41:22」這塊ESP8266模組板在Arduino IDE監控視窗上所看到的畫面內容,除了他本身發送資料的訊息(標記5)之外共有兩筆資料,分別是來自ESP32 MAC位址為「24:6f:28:b1:77:28」(標記1),與「2c:f4:32:17:83:50」(標記2)這塊ESP8266模組板的接收資料,而且當接收到的布林數為’1’時的訊息為”LED點亮” (標記3、4)。
至於下面是本範例系統中MAC位址為「2c:f4:32:17:83:50」這塊ESP8266模組板在Arduino IDE監控視窗上所看到的畫面內容,除了他本身發送資料的訊息(標記5)之外同樣有兩筆資料,分別是來自MAC位址為「a0:20:a6:14:41:22」(標記1)這塊ESP8266模組板,和MAC位址為「24:6f:28:b1:77:28」(標記2)的 ESP32模組板,而且同樣的當接收到的布林數為’1’時會出現” LED點亮”的訊息 (標記3) ,如果布林數為’0’時的訊息改為” LED熄滅” (標記4)。
經由前面說明讀者應該可以發現,雖然說本小節中所示範的『二、6雙向多對多傳輸』這個拓樸結構的通信方式是ESP-NOW中最複雜的,可是所示範的程式反而是比較簡單或者說單純,因為只有一個收發兩用程式而已,不像前一節的範例還必須分別去設計一個發送及接收的程式;此外在接收的資料中我們還示範如何傳送該ESPxx模組板的MAX位址,這樣接收到資料的裝置也可以利用該MAC位址單獨針對特定的發送端回應接收情形或處理的結果。